diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5041f4b625..3b3a442b83 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @cliu123 @cwperks @DarshitChanpura @davidlago @peternied @RyanL1997 @scrawfor99 @reta @willyborankin +* @cliu123 @cwperks @DarshitChanpura @peternied @RyanL1997 @scrawfor99 @reta @willyborankin diff --git a/.github/actions/start-opensearch-with-one-plugin/action.yml b/.github/actions/start-opensearch-with-one-plugin/action.yml deleted file mode 100644 index 642264f4ec..0000000000 --- a/.github/actions/start-opensearch-with-one-plugin/action.yml +++ /dev/null @@ -1,128 +0,0 @@ -name: 'Launch OpenSearch with a single plugin installed' -description: 'Downloads latest build of OpenSearch, installs a plugin, executes a script and then starts OpenSearch on localhost:9200' - -inputs: - opensearch-version: - description: 'The version of OpenSearch that should be used, e.g "3.0.0"' - required: true - - plugin-name: - description: 'The name of the plugin to use, such as opensearch-security' - required: true - - setup-script-name: - 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: - - # Configure longpath names if on Windows - - name: Enable Longpaths if on Windows - if: ${{ runner.os == 'Windows' }} - run: git config --system core.longpaths true - shell: pwsh - - # Download OpenSearch - - name: Download OpenSearch for Windows - uses: peternied/download-file@v2 - if: ${{ runner.os == 'Windows' }} - with: - url: https://artifacts.opensearch.org/snapshots/core/opensearch/${{ inputs.opensearch-version }}-SNAPSHOT/opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-windows-x64-latest.zip - - - - name: Download OpenSearch for Linux - uses: peternied/download-file@v2 - if: ${{ runner.os == 'Linux' }} - with: - url: https://artifacts.opensearch.org/snapshots/core/opensearch/${{ inputs.opensearch-version }}-SNAPSHOT/opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-linux-x64-latest.tar.gz - - # Extract downloaded zip - - name: Extract downloaded tar - if: ${{ runner.os == 'Linux' }} - run: | - tar -xzf opensearch-*.tar.gz - rm -f opensearch-*.tar.gz - shell: bash - - - name: Extract downloaded zip - if: ${{ runner.os == 'Windows' }} - run: | - tar -xzf opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-windows-x64-latest.zip - del opensearch-min-${{ inputs.opensearch-version }}-SNAPSHOT-windows-x64-latest.zip - shell: pwsh - - # Install the plugin - - name: Install Plugin into OpenSearch for Linux - if: ${{ runner.os == 'Linux'}} - run: | - chmod +x ./opensearch-${{ inputs.opensearch-version }}-SNAPSHOT/bin/opensearch-plugin - /bin/bash -c "yes | ./opensearch-${{ inputs.opensearch-version }}-SNAPSHOT/bin/opensearch-plugin install file:$(pwd)/opensearch-security.zip" - shell: bash - - - name: Install Plugin into OpenSearch for Windows - if: ${{ runner.os == 'Windows'}} - run: | - '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 != '' }} - run: | - echo "running linux setup" - chmod +x ./${{ inputs.setup-script-name }}.sh - ./${{ inputs.setup-script-name }}.sh - shell: bash - - - name: Run Setup Script for Windows - if: ${{ runner.os == 'Windows' && inputs.setup-script-name != '' }} - run: .\${{ inputs.setup-script-name }}.bat - shell: pwsh - - # Run OpenSearch - - name: Run OpenSearch with plugin on Linux - if: ${{ runner.os == 'Linux'}} - run: /bin/bash -c "./opensearch-${{ inputs.opensearch-version }}-SNAPSHOT/bin/opensearch &" - shell: bash - - - name: Run OpenSearch with plugin on Windows - if: ${{ runner.os == 'Windows'}} - run: start .\opensearch-${{ inputs.opensearch-version }}-SNAPSHOT\bin\opensearch.bat - shell: pwsh - - # Give the OpenSearch process some time to boot up before sending any requires, might need to increase the default time! - - name: Sleep while OpenSearch starts - uses: peternied/action-sleep@v1 - with: - seconds: 30 - - # 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:${{ 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:${{ inputs.admin-password }}") - $encodedCredentials = [Convert]::ToBase64String($credentialBytes) - $baseCredentials = "Basic $encodedCredentials" - $Headers = @{ Authorization = $baseCredentials } - Invoke-WebRequest -SkipCertificateCheck -Uri 'https://localhost:9200/_cat/plugins' -Headers $Headers; - shell: pwsh - - - if: always() - run: cat ./opensearch-${{ inputs.opensearch-version }}-SNAPSHOT/logs/opensearch.log - shell: bash diff --git a/.github/workflows/add-untriaged.yml b/.github/workflows/add-untriaged.yml index 15b9a55651..864fd26dd5 100644 --- a/.github/workflows/add-untriaged.yml +++ b/.github/workflows/add-untriaged.yml @@ -8,7 +8,7 @@ jobs: apply-label: runs-on: ubuntu-latest steps: - - uses: actions/github-script@v6 + - uses: actions/github-script@v7 with: script: | github.rest.issues.addLabels({ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 399dec5e48..75174cd260 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: separateTestsNames: ${{ steps.set-matrix.outputs.separateTestsNames }} steps: - name: Set up JDK for build and test - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: 17 @@ -45,7 +45,7 @@ jobs: steps: - name: Set up JDK for build and test - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: ${{ matrix.jdk }} @@ -54,13 +54,13 @@ jobs: uses: actions/checkout@v4 - name: Build and Test - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: | ${{ matrix.gradle_task }} -Dbuild.snapshot=false - - uses: alehechka/upload-tartifact@v2 + - uses: actions/upload-artifact@v4 if: always() with: name: ${{ matrix.platform }}-JDK${{ matrix.jdk }}-${{ matrix.gradle_task }}-reports @@ -68,13 +68,11 @@ jobs: ./build/reports/ report-coverage: - needs: - - "test" - - "integration-tests" + needs: ["test", "integration-tests"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4 with: path: downloaded-artifacts @@ -82,23 +80,12 @@ jobs: run: ls -R working-directory: downloaded-artifacts - - name: Extract downloaded artifacts - run: | - for archive in ./*/artifact.tar; do - (cd "$(dirname "$archive")" && tar -xvf artifact.tar) - done - working-directory: downloaded-artifacts - - - name: Display structure of downloaded files - run: ls -R - working-directory: downloaded-artifacts - - name: Upload Coverage with retry - uses: Wandalen/wretry.action@v1.3.0 + uses: Wandalen/wretry.action@v3.4.0 with: attempt_limit: 5 attempt_delay: 2000 - action: codecov/codecov-action@v3 + action: codecov/codecov-action@v4 with: | token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true @@ -111,12 +98,12 @@ jobs: fail-fast: false matrix: jdk: [11, 17, 21] - platform: [ubuntu-latest] # Removed windows https://github.com/opensearch-project/security/issues/3423 + platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: - name: Set up JDK for build and test - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: ${{ matrix.jdk }} @@ -125,7 +112,7 @@ jobs: uses: actions/checkout@v4 - name: Build and Test - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: | @@ -151,7 +138,7 @@ jobs: steps: - name: Set up JDK for build and test - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: ${{ matrix.jdk }} @@ -160,7 +147,7 @@ jobs: uses: actions/checkout@v4 - name: Build and Test - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: | @@ -169,7 +156,7 @@ jobs: backward-compatibility-build: runs-on: ubuntu-latest steps: - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: 17 @@ -178,7 +165,7 @@ jobs: uses: actions/checkout@v4 - name: Build BWC tests - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: | @@ -193,7 +180,7 @@ jobs: runs-on: ${{ matrix.platform }} steps: - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: ${{ matrix.jdk }} @@ -214,22 +201,22 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: 11 - - uses: github/codeql-action/init@v2 + - uses: github/codeql-action/init@v3 with: languages: java - run: ./gradlew clean assemble - - uses: github/codeql-action/analyze@v2 + - uses: github/codeql-action/analyze@v3 build-artifact-names: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: 11 @@ -259,6 +246,8 @@ jobs: - run: ./gradlew clean assemble -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}-SNAPSHOT.zip + - run: ./gradlew clean publishPluginZipPublicationToZipStagingRepository && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.pom + - name: List files in the build directory if there was an error run: ls -al ./build/distributions/ if: failure() diff --git a/.github/workflows/code-hygiene.yml b/.github/workflows/code-hygiene.yml index 1b46c65a63..2f8820709a 100644 --- a/.github/workflows/code-hygiene.yml +++ b/.github/workflows/code-hygiene.yml @@ -19,12 +19,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium - java-version: 11 + java-version: 17 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: spotlessCheck @@ -35,12 +35,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: 11 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: checkstyleMain checkstyleTest checkstyleIntegrationTest @@ -51,12 +51,12 @@ jobs: steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: 11 - - uses: gradle/gradle-build-action@v2 + - uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: spotbugsMain diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 4c2eddcfbc..183276e675 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -15,7 +15,7 @@ jobs: test-run: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] steps: - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: ${{ matrix.jdk }} diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index 1d904020ca..a837289795 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -17,7 +17,7 @@ jobs: contents: write steps: - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: 11 diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml index ae570a9df8..8cfcd156ae 100644 --- a/.github/workflows/plugin_install.yml +++ b/.github/workflows/plugin_install.yml @@ -20,7 +20,7 @@ jobs: uses: peternied/random-name@v1 - name: Set up JDK - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: distribution: temurin # Temurin is a distribution of adoptium java-version: ${{ matrix.jdk }} @@ -29,7 +29,7 @@ jobs: uses: actions/checkout@v4 - name: Assemble target plugin - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: assemble @@ -39,31 +39,16 @@ jobs: run: mv ./build/distributions/${{ env.PLUGIN_NAME }}-*.zip ${{ env.PLUGIN_NAME }}.zip shell: bash - - name: Create Setup Script - if: ${{ runner.os == 'Linux' }} - run: | - cat > setup.sh <<'EOF' - chmod +x ./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/plugins/${{ env.PLUGIN_NAME }}/tools/install_demo_configuration.sh - /bin/bash -c "yes | ./opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT/plugins/${{ env.PLUGIN_NAME }}/tools/install_demo_configuration.sh" - EOF - - - name: Create Setup Script - if: ${{ runner.os == 'Windows' }} - run: | - New-Item .\setup.bat -type file - Set-Content .\setup.bat -Value "powershell.exe .\opensearch-${{ env.OPENSEARCH_VERSION }}-SNAPSHOT\plugins\${{ env.PLUGIN_NAME }}\tools\install_demo_configuration.bat -i -c -y" - Get-Content .\setup.bat - - name: Run Opensearch with A Single Plugin - uses: ./.github/actions/start-opensearch-with-one-plugin + uses: derek-ho/start-opensearch@v4 with: opensearch-version: ${{ env.OPENSEARCH_VERSION }} - plugin-name: ${{ env.PLUGIN_NAME }} - setup-script-name: setup + plugins: "file:$(pwd)/${{ env.PLUGIN_NAME }}.zip" + security-enabled: true admin-password: ${{ steps.random-password.outputs.generated_name }} - name: Run sanity tests - uses: gradle/gradle-build-action@v2 + uses: gradle/gradle-build-action@v3 with: cache-disabled: true 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/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index bf6e5b0674..10e870d04f 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: # Drafts the next Release notes as Pull Requests are merged into "main" - - uses: release-drafter/release-drafter@v5 + - uses: release-drafter/release-drafter@v6 with: config-name: release-notes-drafter-config.yml env: diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 5e797c98d1..4b5a53a8c0 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -86,7 +86,7 @@ rm -rf config/ ## ROOT openssl genrsa -out root-ca-key.pem 2048 -openssl req -new -x509 -sha256 -key root-ca-key.pem -subj "/DC=com/DC=example/O=Example Com Inc./OU=Example Com Inc. Root CA/CN=Example Com Inc. Root CA" -addext "basicConstraints = critical,CA:TRUE" -addext "keyUsage = critical, digitalSignature, keyCertSign, cRLSign" -addext "subjectKeyIdentifier = hash" -addext "authorityKeyIdentifier = keyid:always,issuer:always" -out root-ca.pem +openssl req -new -x509 -sha256 -days 3650 -key root-ca-key.pem -subj "/DC=com/DC=example/O=Example Com Inc./OU=Example Com Inc. Root CA/CN=Example Com Inc. Root CA" -addext "basicConstraints = critical,CA:TRUE" -addext "keyUsage = critical, digitalSignature, keyCertSign, cRLSign" -addext "subjectKeyIdentifier = hash" -addext "authorityKeyIdentifier = keyid:always,issuer:always" -out root-ca.pem ## NODE @@ -94,13 +94,15 @@ openssl req -new -x509 -sha256 -key root-ca-key.pem -subj "/DC=com/DC=example/O= openssl genrsa -out esnode-key-temp.pem 2048 openssl pkcs8 -inform PEM -outform PEM -in esnode-key-temp.pem -topk8 -nocrypt -v1 PBE-SHA1-3DES -out esnode-key.pem openssl req -new -key esnode-key.pem -subj "/C=de/L=test/O=node/OU=node/CN=node-0.example.com" -out esnode.csr -openssl x509 -req -in esnode.csr -out esnode.pem -CA root-ca.pem -CAkey root-ca-key.pem -CAcreateserial -days 3650 -extfile <(printf "subjectAltName = RID:1.2.3.4.5.5, DNS:node-0.example.com, DNS:localhost, IP:::1, IP:127.0.0.1\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment\nextendedKeyUsage = serverAuth, clientAuth\nbasicConstraints = critical,CA:FALSE") +printf "subjectAltName = RID:1.2.3.4.5.5, DNS:node-0.example.com, DNS:localhost, IP:::1, IP:127.0.0.1\nkeyUsage = digitalSignature, nonRepudiation, keyEncipherment\nextendedKeyUsage = serverAuth, clientAuth\nbasicConstraints = critical,CA:FALSE" > esnode_ext.conf +openssl x509 -req -in esnode.csr -out esnode.pem -CA root-ca.pem -CAkey root-ca-key.pem -CAcreateserial -days 3650 -extfile esnode_ext.conf ## ADMIN openssl req -new -newkey rsa:2048 -keyout kirk-key.pem -out kirk.csr -nodes -subj "/C=de/L=test/O=client/OU=client/CN=kirk" -openssl x509 -req -in kirk.csr -CA root-ca.pem -CAkey root-ca-key.pem -CAcreateserial -out kirk.pem -days 3650 -extfile <(printf "basicConstraints = critical,CA:FALSE\nkeyUsage = critical,digitalSignature,nonRepudiation,keyEncipherment\nextendedKeyUsage = critical,clientAuth\nauthorityKeyIdentifier = keyid,issuer:always\nsubjectKeyIdentifier = hash") +printf "basicConstraints = critical,CA:FALSE\nkeyUsage = critical,digitalSignature,nonRepudiation,keyEncipherment\nextendedKeyUsage = critical,clientAuth\nauthorityKeyIdentifier=keyid,issuer:always\nsubjectKeyIdentifier = hash" > kirk_ext.conf +openssl x509 -req -in kirk.csr -CA root-ca.pem -CAkey root-ca-key.pem -CAcreateserial -out kirk.pem -days 3650 -extfile kirk_ext.conf ## Remove root-ca-key.pem and other temp keys @@ -162,6 +164,18 @@ extension_hw_greet: - "hw-user" ``` +### Setting up password for demo admin user + +This step is a pre-requisite to installing demo configuration. You can pass the demo `admin` user password by exporting `OPENSEARCH_INITIAL_ADMIN_PASSWORD` variable with a password. +```shell +export OPENSEARCH_INITIAL_ADMIN_PASSWORD= +``` + +**_Note:_** If no password is supplied, the installation will fail. The password supplied will also be tested for its strength and will be blocked if it is too simple. There is an option to skip this password validation by passing the `-t` option to the installation script. However, this should only be used for test environments. + + +### Executing the demo installation script + To install the demo certificates and default configuration, answer `y` to the first two questions and `n` to the last one. The log should look like below: ```bash @@ -192,17 +206,17 @@ Detected OpenSearch Security Version: * "/Users/XXXXX/Test/opensearch-*/plugins/opensearch-security/tools/securityadmin.sh" -cd "/Users/XXXXX/Test/opensearch-*/config/opensearch-security/" -icl -key "/Users/XXXXX/Test/opensearch-*/config/kirk-key.pem" -cert "/Users/XXXXX/Test/opensearch-*/config/kirk.pem" -cacert "/Users/XXXXX/Test/opensearch-*/config/root-ca.pem" -nhnv ### or run ./securityadmin_demo.sh ### To use the Security Plugin ConfigurationGUI -### To access your secured cluster open https://: and log in with admin/admin. +### To access your secured cluster open https://: and log in with admin/. ### (Ignore the SSL certificate warning because we installed self-signed demo certificates) ``` Now if we start our server again and try the original `curl localhost:9200`, it will fail. -Try this command instead: `curl -XGET https://localhost:9200 -u 'admin:admin' --insecure`. It should succeed. +Try this command instead: `curl -XGET https://localhost:9200 -u 'admin:' --insecure`. It should succeed. You can also make this call to return the authenticated user details: ```bash -curl -XGET https://localhost:9200/_plugins/_security/authinfo -u 'admin:admin' --insecure +curl -XGET https://localhost:9200/_plugins/_security/authinfo -u 'admin:' --insecure { "user": "User [name=admin, backend_roles=[admin], requestedTenant=null]", @@ -232,11 +246,30 @@ curl -XGET https://localhost:9200/_plugins/_security/authinfo -u 'admin:admin' - Launch IntelliJ IDEA, choose **Project from Existing Sources**, and select directory with Gradle build script (`build.gradle`). -## Running integration tests +## Running tests Locally these can be run with `./gradlew test` with detailed results being available at `${project-root}/build/reports/tests/test/index.html`. You can also run tests through an IDEs JUnit test runner. -Integration tests are automatically run on all pull requests for all supported versions of the JDK. These must pass for change(s) to be merged. Detailed logs of these test results are available by going to the GitHub Actions workflow summary view and downloading the workflow run of the tests. If you see multiple tests listed with different JDK versions, you can download the version with whichever JDK you are interested in. After extracting the test file on your local machine, integration tests results can be found at `./tests/tests/index.html`. +Tests are automatically run on all pull requests for all supported versions of the JDK. These must pass for change(s) to be merged. Detailed logs of these test results are available by going to the GitHub Actions workflow summary view and downloading the workflow run of the tests. If you see multiple tests listed with different JDK versions, you can download the version with whichever JDK you are interested in. After extracting the test file on your local machine, integration tests results can be found at `./tests/tests/index.html`. + +### Running an individual test multiple times + +This repo has a `@Repeat` annotation which you can import to annotate a test to run many times repeatedly. To use the annotation, add the following code to your test suite. + +``` +@Rule +public RepeatRule repeatRule = new RepeatRule(); + +@Test +@Repeat(10) +public void testMethod() { + ... +} +``` + +## Running tests in the integrationTest package + +Tests in the integrationTest package can be run with `./gradlew integrationTest`. ### Bulk test runs diff --git a/MAINTAINERS.md b/MAINTAINERS.md index f27e192923..bafadc11bd 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -16,7 +16,6 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | ---------------- | ----------------------------------------------------- | ----------- | | Chang Liu | [cliu123](https://github.com/cliu123) | Amazon | | Darshit Chanpura | [DarshitChanpura](https://github.com/DarshitChanpura) | Amazon | -| Dave Lago | [davidlago](https://github.com/davidlago) | Amazon | | Peter Nied | [peternied](https://github.com/peternied) | Amazon | | Craig Perkins | [cwperks](https://github.com/cwperks) | Amazon | | Ryan Liang | [RyanL1997](https://github.com/RyanL1997) | Amazon | @@ -24,6 +23,12 @@ This document contains a list of maintainers in this repo. See [opensearch-proje | Andriy Redko | [reta](https://github.com/reta) | Aiven | | Andrey Pleskach | [willyborankin](https://github.com/willyborankin) | Aiven | +## Emeritus + +| Maintainer | GitHub ID | Affiliation | +| ------------- | --------------------------------------------------- | ----------- | +| Dave Lago | [davidlago](https://github.com/davidlago) | Contributor | + ## Practices ### Updating Practices diff --git a/README.md b/README.md index 1aed7a5a0b..0d5d81e109 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,6 @@ Run tests against local cluster: ```bash ./gradlew integTestRemote -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=docker-cluster -Dsecurity=true -Dhttps=true -Duser=admin -Dpassword=admin -Dcommon_utils.version="2.2.0.0" ``` -OR -```bash -./scripts/integtest.sh -``` Note: To run against a remote cluster replace cluster-name and `localhost:9200` with the IPAddress:Port of that cluster. Build artifacts (zip, deb, rpm): @@ -133,8 +129,7 @@ plugins.security.system_indices.indices: [".plugins-ml-model", ".plugins-ml-task The demo configuration can be modified in the following files to add a new system index to the demo configuration: -- https://github.com/opensearch-project/security/blob/main/tools/install_demo_configuration.sh -- https://github.com/opensearch-project/security/blob/main/tools/install_demo_configuration.bat +- https://github.com/opensearch-project/security/blob/main/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java ## Contributing diff --git a/THIRD-PARTY.txt b/THIRD-PARTY.txt deleted file mode 100644 index d8a027321b..0000000000 --- a/THIRD-PARTY.txt +++ /dev/null @@ -1,71 +0,0 @@ - -Lists of 69 third-party dependencies. - (The Apache Software License, Version 2.0) HPPC Collections (com.carrotsearch:hppc:0.7.1 - http://labs.carrotsearch.com/hppc.html/hppc) - (The Apache Software License, Version 2.0) Jackson-core (com.fasterxml.jackson.core:jackson-core:2.8.10 - https://github.com/FasterXML/jackson-core) - (The Apache Software License, Version 2.0) Jackson dataformat: CBOR (com.fasterxml.jackson.dataformat:jackson-dataformat-cbor:2.8.10 - http://github.com/FasterXML/jackson-dataformats-binary) - (The Apache Software License, Version 2.0) Jackson dataformat: Smile (com.fasterxml.jackson.dataformat:jackson-dataformat-smile:2.8.10 - http://github.com/FasterXML/jackson-dataformats-binary) - (The Apache Software License, Version 2.0) Jackson-dataformat-YAML (com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.8.10 - https://github.com/FasterXML/jackson) - (The Apache Software License, Version 2.0) OpenDistro Security SSL (com.amazon.opendistroforelasticsearch:opendistro-elasticsearch-security-ssl:0.0.8.0 - https://github.com/opendistro-for-elasticsearch/security-ssl) - (Apache License 2.0) compiler (com.github.spullara.mustache.java:compiler:0.9.3 - http://github.com/spullara/mustache.java) - (The Apache Software License, Version 2.0) FindBugs-jsr305 (com.google.code.findbugs:jsr305:1.3.9 - http://findbugs.sourceforge.net/) - (Apache 2.0) error-prone annotations (com.google.errorprone:error_prone_annotations:2.0.18 - http://nexus.sonatype.org/oss-repository-hosting.html/error_prone_parent/error_prone_annotations) - (The Apache Software License, Version 2.0) Guava: Google Core Libraries for Java (com.google.guava:guava:23.0 - https://github.com/google/guava/guava) - (The Apache Software License, Version 2.0) J2ObjC Annotations (com.google.j2objc:j2objc-annotations:1.1 - https://github.com/google/j2objc/) - (The Apache Software License, Version 2.0) T-Digest (com.tdunning:t-digest:3.0 - https://github.com/tdunning/t-digest) - (Lesser General Public License (LGPL)) JTS Topology Suite (com.vividsolutions:jts:1.13 - http://sourceforge.net/projects/jts-topo-suite) - (Apache License, Version 2.0) Apache Commons CLI (commons-cli:commons-cli:1.3.1 - http://commons.apache.org/proper/commons-cli/) - (Apache License, Version 2.0) Apache Commons Codec (commons-codec:commons-codec:1.10 - http://commons.apache.org/proper/commons-codec/) - (Apache License, Version 2.0) Apache Commons IO (commons-io:commons-io:2.6 - http://commons.apache.org/proper/commons-io/) - (The Apache Software License, Version 2.0) Commons Logging (commons-logging:commons-logging:1.1.3 - http://commons.apache.org/proper/commons-logging/) - (Apache License, Version 2.0) Netty/Buffer (io.netty:netty-buffer:4.1.16.Final - http://netty.io/netty-buffer/) - (Apache License, Version 2.0) Netty/Codec (io.netty:netty-codec:4.1.16.Final - http://netty.io/netty-codec/) - (Apache License, Version 2.0) Netty/Codec/HTTP (io.netty:netty-codec-http:4.1.16.Final - http://netty.io/netty-codec-http/) - (Apache License, Version 2.0) Netty/Common (io.netty:netty-common:4.1.16.Final - http://netty.io/netty-common/) - (Apache License, Version 2.0) Netty/Handler (io.netty:netty-handler:4.1.16.Final - http://netty.io/netty-handler/) - (Apache License, Version 2.0) Netty/Resolver (io.netty:netty-resolver:4.1.16.Final - http://netty.io/netty-resolver/) - (Apache License, Version 2.0) Netty/TomcatNative [OpenSSL - Dynamic] (io.netty:netty-tcnative:2.0.7.Final - https://github.com/netty/netty-tcnative/netty-tcnative/) - (Apache License, Version 2.0) Netty/Transport (io.netty:netty-transport:4.1.16.Final - http://netty.io/netty-transport/) - (Apache 2) Joda-Time (joda-time:joda-time:2.9.9 - http://www.joda.org/joda-time/) - (Eclipse Public License 1.0) JUnit (junit:junit:4.12 - http://junit.org) - (The MIT License) JOpt Simple (net.sf.jopt-simple:jopt-simple:5.0.2 - http://pholser.github.io/jopt-simple) - (Apache License, Version 2.0) Apache HttpAsyncClient (org.apache.httpcomponents:httpasyncclient:4.1.2 - http://hc.apache.org/httpcomponents-asyncclient) - (Apache License, Version 2.0) Apache HttpClient (org.apache.httpcomponents:httpclient:4.5.2 - http://hc.apache.org/httpcomponents-client) - (Apache License, Version 2.0) Apache HttpCore (org.apache.httpcomponents:httpcore:4.4.5 - http://hc.apache.org/httpcomponents-core-ga) - (Apache License, Version 2.0) Apache HttpCore NIO (org.apache.httpcomponents:httpcore-nio:4.4.5 - http://hc.apache.org/httpcomponents-core-ga) - (Apache License, Version 2.0) Apache Log4j API (org.apache.logging.log4j:log4j-api:2.9.1 - https://logging.apache.org/log4j/2.x/log4j-api/) - (Apache License, Version 2.0) Apache Log4j Core (org.apache.logging.log4j:log4j-core:2.9.1 - https://logging.apache.org/log4j/2.x/log4j-core/) - (Apache 2) Lucene Common Analyzers (org.apache.lucene:lucene-analyzers-common:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-analyzers-common) - (Apache 2) Lucene Memory (org.apache.lucene:lucene-backward-codecs:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-backward-codecs) - (Apache 2) Lucene Core (org.apache.lucene:lucene-core:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-core) - (Apache 2) Lucene Grouping (org.apache.lucene:lucene-grouping:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-grouping) - (Apache 2) Lucene Highlighter (org.apache.lucene:lucene-highlighter:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-highlighter) - (Apache 2) Lucene Join (org.apache.lucene:lucene-join:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-join) - (Apache 2) Lucene Memory (org.apache.lucene:lucene-memory:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-memory) - (Apache 2) Lucene Miscellaneous (org.apache.lucene:lucene-misc:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-misc) - (Apache 2) Lucene Queries (org.apache.lucene:lucene-queries:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-queries) - (Apache 2) Lucene QueryParsers (org.apache.lucene:lucene-queryparser:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-queryparser) - (Apache 2) Lucene Sandbox (org.apache.lucene:lucene-sandbox:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-sandbox) - (Apache 2) Lucene Spatial (org.apache.lucene:lucene-spatial:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-spatial) - (Apache 2) Lucene Spatial Extras (org.apache.lucene:lucene-spatial-extras:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-spatial-extras) - (Apache 2) Lucene Spatial 3D (org.apache.lucene:lucene-spatial3d:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-spatial3d) - (Apache 2) Lucene Suggest (org.apache.lucene:lucene-suggest:7.2.1 - http://lucene.apache.org/lucene-parent/lucene-suggest) - (Apache Software License, Version 1.1) (Bouncy Castle Licence) Bouncy Castle OpenPGP API (org.bouncycastle:bcpg-jdk15on:1.59 - http://www.bouncycastle.org/java.html) - (Bouncy Castle Licence) Bouncy Castle Provider (org.bouncycastle:bcprov-jdk15on:1.59 - http://www.bouncycastle.org/java.html) - (MIT license) Animal Sniffer Annotations (org.codehaus.mojo:animal-sniffer-annotations:1.14 - http://mojo.codehaus.org/animal-sniffer/animal-sniffer-annotations) - (The Apache Software License, Version 2.0) server (org.elasticsearch:elasticsearch:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) cli (org.elasticsearch:elasticsearch-cli:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) elasticsearch-core (org.elasticsearch:elasticsearch-core:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) java native access (net.java.dev.jna:jna:5.5.0 - https://github.com/java-native-access/jna) - (The Apache Software License, Version 2.0) Elasticsearch SecureSM (org.elasticsearch:securesm:1.2 - http://nexus.sonatype.org/oss-repository-hosting.html/securesm) - (The Apache Software License, Version 2.0) rest (org.elasticsearch.client:elasticsearch-rest-client:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) aggs-matrix-stats (org.elasticsearch.plugin:aggs-matrix-stats-client:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) lang-mustache (org.elasticsearch.plugin:lang-mustache-client:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) parent-join (org.elasticsearch.plugin:parent-join-client:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) percolator (org.elasticsearch.plugin:percolator-client:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) reindex (org.elasticsearch.plugin:reindex-client:6.2.0 - https://github.com/elastic/elasticsearch) - (The Apache Software License, Version 2.0) transport-netty4 (org.elasticsearch.plugin:transport-netty4-client:6.2.0 - https://github.com/elastic/elasticsearch) - (New BSD License) Hamcrest All (org.hamcrest:hamcrest-all:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-all) - (New BSD License) Hamcrest Core (org.hamcrest:hamcrest-core:1.3 - https://github.com/hamcrest/JavaHamcrest/hamcrest-core) - (Public Domain, per Creative Commons CC0) HdrHistogram (org.hdrhistogram:HdrHistogram:2.1.9 - http://hdrhistogram.github.io/HdrHistogram/) - (The Apache Software License, Version 2.0) Spatial4J (org.locationtech.spatial4j:spatial4j:0.6 - http://www.locationtech.org/projects/locationtech.spatial4j) - (Apache License, Version 2.0) SnakeYAML (org.yaml:snakeyaml:1.17 - http://www.snakeyaml.org) diff --git a/TRIAGING.md b/TRIAGING.md index 22e59afc2d..7f70501dae 100644 --- a/TRIAGING.md +++ b/TRIAGING.md @@ -12,7 +12,7 @@ Each meeting we seek to address all new issues. However, should we run out of t ### How do I join the Backlog & Triage meeting? -Meetings are hosted regularly at 3 PM Eastern Time (Noon Pacific Time) and can be joined via the links posted on the [Upcoming Events](https://opensearch.org/events) webpage. +Meetings are hosted regularly at 11 AM Eastern Time (8AM Pacific Time) and can be joined via the links posted on the [OpenSearch Meetup Group](https://www.meetup.com/opensearch/events/) list of events. The event will be titled `Development Backlog & Triage Meeting - Security`. After joining the Zoom meeting, you can enable your video / voice to join the discussion. If you do not have a webcam or microphone available, you can still join in via the text chat. diff --git a/build.gradle b/build.gradle index ef4a33edb0..4fe975e00b 100644 --- a/build.gradle +++ b/build.gradle @@ -25,13 +25,14 @@ buildscript { opensearch_build = version_tokens[0] + '.0' common_utils_version = System.getProperty("common_utils.version", '3.0.0.0-SNAPSHOT') - kafka_version = '3.6.0' - apache_cxf_version = '4.0.3' - open_saml_version = '4.3.0' + kafka_version = '3.7.0' + apache_cxf_version = '4.0.4' + open_saml_version = '4.3.2' one_login_java_saml = '2.9.0' - jjwt_version = '0.12.3' + jjwt_version = '0.12.5' guava_version = '32.1.3-jre' jaxb_version = '2.3.9' + spring_version = '5.3.34' if (buildVersionQualifier) { opensearch_build += "-${buildVersionQualifier}" @@ -46,7 +47,7 @@ buildscript { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } - maven { url "https://d1nvenhzbhpy0q.cloudfront.net/snapshots/lucene/" } + maven { url "https://artifacts.opensearch.org/snapshots/lucene/" } maven { url "https://build.shibboleth.net/nexus/content/groups/public" } maven { url "https://build.shibboleth.net/nexus/content/repositories/releases" } } @@ -61,12 +62,12 @@ plugins { id 'idea' id 'jacoco' id 'maven-publish' - id 'com.diffplug.spotless' version '6.22.0' + id 'com.diffplug.spotless' version '6.25.0' id 'checkstyle' - id 'com.netflix.nebula.ospackage' version "11.5.0" - id "org.gradle.test-retry" version "1.5.6" + id 'com.netflix.nebula.ospackage' version "11.9.0" + id "org.gradle.test-retry" version "1.5.9" id 'eclipse' - id "com.github.spotbugs" version "5.2.3" + id "com.github.spotbugs" version "5.2.5" id "com.google.osdetector" version "1.7.3" } @@ -255,6 +256,8 @@ test { jvmArgs += "-Xmx3072m" if (JavaVersion.current() > JavaVersion.VERSION_1_8) { jvmArgs += "--add-opens=java.base/java.io=ALL-UNNAMED" + // this is needed to reflect access system env map. + jvmArgs += "--add-opens=java.base/java.util=ALL-UNNAMED" } retry { failOnPassedAfterRetry = false @@ -302,6 +305,8 @@ def setCommonTestConfig(Test task) { task.jvmArgs += "-Xmx3072m" if (JavaVersion.current() > JavaVersion.VERSION_1_8) { task.jvmArgs += "--add-opens=java.base/java.io=ALL-UNNAMED" + // this is needed to reflect access system env map. + task.jvmArgs += "--add-opens=java.base/java.util=ALL-UNNAMED" } task.retry { failOnPassedAfterRetry = false @@ -386,6 +391,7 @@ checkstyle { } tasks.withType(Checkstyle) { + dependsOn(':precommit') reports { ignoreFailures = false } @@ -401,11 +407,6 @@ opensearchplugin { // This requires an additional Jar not published as part of build-tools loggerUsageCheck.enabled = false -// No need to validate pom, as we do not upload to maven/sonatype -tasks.matching {it.path in [":validateMavenPom", ":validateNebulaPom", ":validatePluginZipPom"]}.all { task -> - task.dependsOn ':generatePomFileForNebulaPublication', ':generatePomFileForPluginZipPublication', ':generatePomFileForMavenPublication' -} - publishing { publications { pluginZip(MavenPublication) { publication -> @@ -445,7 +446,7 @@ repositories { mavenCentral() maven { url "https://plugins.gradle.org/m2/" } maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } - maven { url "https://d1nvenhzbhpy0q.cloudfront.net/snapshots/lucene/" } + maven { url "https://artifacts.opensearch.org/snapshots/lucene/" } maven { url "https://build.shibboleth.net/nexus/content/repositories/releases" } } @@ -468,9 +469,9 @@ bundlePlugin { configurations { all { resolutionStrategy { - force 'commons-codec:commons-codec:1.16.0' + force 'commons-codec:commons-codec:1.17.0' force 'org.slf4j:slf4j-api:1.7.36' - force 'org.scala-lang:scala-library:2.13.12' + force 'org.scala-lang:scala-library:2.13.13' force "com.fasterxml.jackson:jackson-bom:${versions.jackson}" force "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" force "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${versions.jackson}" @@ -487,11 +488,15 @@ configurations { // for spotbugs dependency conflict force "org.apache.commons:commons-lang3:${versions.commonslang}" + // for spotless transitive dependency CVE + force "org.eclipse.platform:org.eclipse.core.runtime:3.31.0" + // For integrationTest force "org.apache.httpcomponents:httpclient:4.5.14" force "org.apache.httpcomponents:httpcore:4.4.16" - force "com.google.errorprone:error_prone_annotations:2.23.0" - force "org.checkerframework:checker-qual:3.40.0" + force "com.google.errorprone:error_prone_annotations:2.27.0" + force "org.checkerframework:checker-qual:3.42.0" + force "ch.qos.logback:logback-classic:1.5.6" } } @@ -557,6 +562,8 @@ task integrationTest(type: Test) { } } +tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generated after integration tests run + //run the integrationTest task before the check task check.dependsOn integrationTest @@ -570,23 +577,17 @@ dependencies { implementation "com.google.guava:guava:${guava_version}" implementation 'org.greenrobot:eventbus-java:3.3.1' implementation 'commons-cli:commons-cli:1.6.0' - implementation "org.bouncycastle:bcprov-jdk15to18:${versions.bouncycastle}" + implementation "org.bouncycastle:bcprov-jdk18on:${versions.bouncycastle}" implementation 'org.ldaptive:ldaptive:1.2.3' - implementation 'com.nimbusds:nimbus-jose-jwt:9.37' + implementation 'com.nimbusds:nimbus-jose-jwt:9.37.3' + implementation 'com.rfksystems:blake2b:2.0.0' //JWT implementation "io.jsonwebtoken:jjwt-api:${jjwt_version}" 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.3") { - exclude group: "org.glassfish", module: "jakarta.json" - exclude group: "com.google.code.gson", module: "gson" - exclude group: "org.json", module: "json" - } - implementation 'com.github.wnameless.json:json-flattener:0.16.6' // JSON patch - implementation 'com.flipkart.zjsonpatch:zjsonpatch:0.4.14' + implementation 'com.flipkart.zjsonpatch:zjsonpatch:0.4.16' implementation 'org.apache.commons:commons-collections4:4.4' //Password generation @@ -594,7 +595,7 @@ dependencies { implementation "org.apache.kafka:kafka-clients:${kafka_version}" - runtimeOnly 'net.minidev:accessors-smart:2.5.0' + runtimeOnly 'net.minidev:accessors-smart:2.5.1' runtimeOnly "org.apache.cxf:cxf-core:${apache_cxf_version}" implementation "org.apache.cxf:cxf-rt-rs-json-basic:${apache_cxf_version}" @@ -602,17 +603,18 @@ dependencies { runtimeOnly 'com.sun.activation:jakarta.activation:1.2.2' runtimeOnly 'com.eclipsesource.minimal-json:minimal-json:0.9.5' - runtimeOnly 'commons-codec:commons-codec:1.16.0' + runtimeOnly 'commons-codec:commons-codec:1.17.0' runtimeOnly 'org.cryptacular:cryptacular:1.2.6' - compileOnly 'com.google.errorprone:error_prone_annotations:2.23.0' + compileOnly 'com.google.errorprone:error_prone_annotations:2.27.0' runtimeOnly 'com.sun.istack:istack-commons-runtime:4.2.0' - runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.1' - runtimeOnly 'org.ow2.asm:asm:9.6' + runtimeOnly 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.2' + runtimeOnly 'org.ow2.asm:asm:9.7' - testImplementation 'org.apache.camel:camel-xmlsecurity:3.21.2' + testImplementation 'org.apache.camel:camel-xmlsecurity:3.22.1' //OpenSAML - implementation 'net.shibboleth.utilities:java-support:8.4.0' + implementation 'net.shibboleth.utilities:java-support:8.4.2' + runtimeOnly "io.dropwizard.metrics:metrics-core:4.2.25" implementation "com.onelogin:java-saml:${one_login_java_saml}" implementation "com.onelogin:java-saml-core:${one_login_java_saml}" implementation "org.opensaml:opensaml-core:${open_saml_version}" @@ -630,26 +632,25 @@ dependencies { runtimeOnly "org.opensaml:opensaml-soap-impl:${open_saml_version}" implementation "org.opensaml:opensaml-storage-api:${open_saml_version}" - implementation "com.nulab-inc:zxcvbn:1.8.2" + implementation "com.nulab-inc:zxcvbn:1.9.0" runtimeOnly 'com.google.guava:failureaccess:1.0.2' - runtimeOnly 'org.apache.commons:commons-text:1.11.0' + runtimeOnly 'org.apache.commons:commons-text:1.12.0' runtimeOnly "org.glassfish.jaxb:jaxb-runtime:${jaxb_version}" runtimeOnly 'com.google.j2objc:j2objc-annotations:2.8' compileOnly 'com.google.code.findbugs:jsr305:3.0.2' runtimeOnly 'org.lz4:lz4-java:1.8.0' - runtimeOnly 'io.dropwizard.metrics:metrics-core:4.2.22' runtimeOnly 'org.slf4j:slf4j-api:1.7.36' runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:${versions.log4j}" runtimeOnly 'org.xerial.snappy:snappy-java:1.1.10.5' runtimeOnly 'org.codehaus.woodstox:stax2-api:4.2.2' runtimeOnly "org.glassfish.jaxb:txw2:${jaxb_version}" - runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.5.1' + runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.6.2' runtimeOnly 'org.apache.ws.xmlschema:xmlschema-core:2.3.1' runtimeOnly 'org.apache.santuario:xmlsec:2.3.4' runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" - runtimeOnly 'org.checkerframework:checker-qual:3.40.0' - runtimeOnly "org.bouncycastle:bcpkix-jdk15to18:${versions.bouncycastle}" + runtimeOnly 'org.checkerframework:checker-qual:3.42.0' + runtimeOnly "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" runtimeOnly 'org.scala-lang.modules:scala-java8-compat_3:1.0.2' @@ -672,16 +673,20 @@ dependencies { testImplementation 'org.apache.httpcomponents:fluent-hc:4.5.14' testImplementation "org.apache.httpcomponents.client5:httpclient5-fluent:${versions.httpclient5}" testImplementation "org.apache.kafka:kafka_2.13:${kafka_version}" + testImplementation "org.apache.kafka:kafka-server:${kafka_version}" testImplementation "org.apache.kafka:kafka-server-common:${kafka_version}" testImplementation "org.apache.kafka:kafka-server-common:${kafka_version}:test" testImplementation "org.apache.kafka:kafka-group-coordinator:${kafka_version}" testImplementation "org.apache.kafka:kafka_2.13:${kafka_version}:test" testImplementation "org.apache.kafka:kafka-clients:${kafka_version}:test" - testImplementation 'commons-validator:commons-validator:1.7' + testImplementation 'commons-validator:commons-validator:1.8.0' testImplementation 'org.springframework.kafka:spring-kafka-test:2.9.13' - testImplementation 'org.springframework:spring-beans:5.3.30' - testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0' - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.0' + testImplementation "org.springframework:spring-beans:${spring_version}" + testImplementation 'org.junit.jupiter:junit-jupiter:5.10.2' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.10.2' + testImplementation('org.awaitility:awaitility:4.2.1') { + exclude(group: 'org.hamcrest', module: 'hamcrest') + } // Only osx-x86_64, osx-aarch_64, linux-x86_64, linux-aarch_64, windows-x86_64 are available if (osdetector.classifier in ["osx-x86_64", "osx-aarch_64", "linux-x86_64", "linux-aarch_64", "windows-x86_64"]) { testImplementation "io.netty:netty-tcnative-classes:2.0.61.Final" @@ -691,15 +696,16 @@ 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.30') { + testRuntimeOnly ("org.springframework:spring-core:${spring_version}") { exclude(group:'org.springframework', module: 'spring-jcl' ) } - testRuntimeOnly 'org.scala-lang:scala-library:2.13.12' - testRuntimeOnly 'com.yammer.metrics:metrics-core:2.2.0' + testRuntimeOnly 'org.scala-lang:scala-library:2.13.13' testRuntimeOnly 'com.typesafe.scala-logging:scala-logging_3:3.9.5' - testRuntimeOnly('org.apache.zookeeper:zookeeper:3.9.1') { + testRuntimeOnly('org.apache.zookeeper:zookeeper:3.9.2') { exclude(group:'ch.qos.logback', module: 'logback-classic' ) + exclude(group:'ch.qos.logback', module: 'logback-core' ) } + testRuntimeOnly 'com.yammer.metrics:metrics-core:2.2.0' testRuntimeOnly "org.apache.kafka:kafka-metadata:${kafka_version}" testRuntimeOnly "org.apache.kafka:kafka-storage:${kafka_version}" @@ -715,13 +721,13 @@ dependencies { integrationTestImplementation 'junit:junit:4.13.2' integrationTestImplementation "org.opensearch.plugin:reindex-client:${opensearch_version}" integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" - integrationTestImplementation 'commons-io:commons-io:2.15.0' + integrationTestImplementation 'commons-io:commons-io:2.16.1' integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}" integrationTestImplementation 'org.hamcrest:hamcrest:2.2' - integrationTestImplementation "org.bouncycastle:bcpkix-jdk15to18:${versions.bouncycastle}" - integrationTestImplementation "org.bouncycastle:bcutil-jdk15to18:${versions.bouncycastle}" - integrationTestImplementation('org.awaitility:awaitility:4.2.0') { + integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" + integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}" + integrationTestImplementation('org.awaitility:awaitility:4.2.1') { exclude(group: 'org.hamcrest', module: 'hamcrest') } integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14' @@ -732,7 +738,7 @@ dependencies { integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" //spotless - implementation('com.google.googlejavaformat:google-java-format:1.17.0') { + implementation('com.google.googlejavaformat:google-java-format:1.22.0') { exclude group: 'com.google.guava' } } diff --git a/bwc-test/src/test/resources/security/esnode.pem b/bwc-test/src/test/resources/security/esnode.pem index 12801ce5e7..b690a603da 100644 --- a/bwc-test/src/test/resources/security/esnode.pem +++ b/bwc-test/src/test/resources/security/esnode.pem @@ -1,9 +1,9 @@ -----BEGIN CERTIFICATE----- -MIIEPDCCAySgAwIBAgIUZjrlDPP8azRDPZchA/XEsx0X2iMwDQYJKoZIhvcNAQEL +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yMzA4MjkxOTQ0NDJaFw0zMzA4MjYxOTQ0NDJaMFcxCzAJBgNVBAYT +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud @@ -16,10 +16,10 @@ BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ -KoZIhvcNAQELBQADggEBAHfKc6NUXJfsT5nL1CtaGy3dutV13UQkdUwEkB0BikkX -1PNaz2NeHyO2yQvp4G6WlovZB78tVqn5hbEZL7v8kUlAOTkjEJjsOu1Ib746eBdT -gmUBKpIeBrm3a+tsLR9OBOuDb8aQO6fnFehFs/70y0sbyRbVqSmxLaYgRkPhhqwl -3U7Ha1TpdJrckETk/iRcma0igvym1SvlUahgFXN4ZCLG3SycH+YRFtM749GVZBo5 -5E5gSfkWCj9jao3LjJn3ThtMsiL405uIPbFNm+5iXtflMk2aW666j1jlpeaZySuy -DWBtA+T5Y6HhDECSjHOV131UekvHLF+SbWrv0S+ptjA= +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= -----END CERTIFICATE----- diff --git a/bwc-test/src/test/resources/security/kirk.pem b/bwc-test/src/test/resources/security/kirk.pem index 716b4ec4d9..b89edfe18f 100644 --- a/bwc-test/src/test/resources/security/kirk.pem +++ b/bwc-test/src/test/resources/security/kirk.pem @@ -1,9 +1,9 @@ -----BEGIN CERTIFICATE----- -MIIEmDCCA4CgAwIBAgIUZjrlDPP8azRDPZchA/XEsx0X2iYwDQYJKoZIhvcNAQEL +MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yMzA4MjkyMDA2MzdaFw0zMzA4MjYyMDA2MzdaME0xCzAJBgNVBAYT +dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs @@ -12,16 +12,16 @@ O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw -DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMIHPBgNVHSME -gccwgcSAFBeH36Ba62YSp9XQ+LoSRTy3KwCcoYGVpIGSMIGPMRMwEQYKCZImiZPy -LGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhh -bXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENB -MSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCFHfkrz782p+T9k0G -xGeM4+BrehWKMB0GA1UdDgQWBBSjMS8tgguX/V7KSGLoGg7K6XMzIDANBgkqhkiG -9w0BAQsFAAOCAQEANMwD1JYlwAh82yG1gU3WSdh/tb6gqaSzZK7R6I0L7slaXN9m -y2ErUljpTyaHrdiBFmPhU/2Kj2r+fIUXtXdDXzizx/JdmueT0nG9hOixLqzfoC9p -fAhZxM62RgtyZoaczQN82k1/geMSwRpEndFe3OH7arkS/HSbIFxQhAIy229eWe5d -1bUzP59iu7f3r567I4ob8Vy7PP+Ov35p7Vv4oDHHwgsdRzX6pvL6mmwVrQ3BfVec -h9Dqprr+ukYmjho76g6k5cQuRaB6MxqldzUg+2E7IHQP8MCF+co51uZq2nl33mtp -RGr6JbdHXc96zsLTL3saJQ8AWEfu1gbTVrwyRA== +DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW +BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV +0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS +JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf +BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs +ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG +9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8 +Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl +1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy +KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9 +E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/ +e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ== -----END CERTIFICATE----- diff --git a/bwc-test/src/test/resources/security/root-ca.pem b/bwc-test/src/test/resources/security/root-ca.pem index 5948a73b30..854323e6fe 100644 --- a/bwc-test/src/test/resources/security/root-ca.pem +++ b/bwc-test/src/test/resources/security/root-ca.pem @@ -1,9 +1,9 @@ -----BEGIN CERTIFICATE----- -MIIExjCCA66gAwIBAgIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcNAQEL +MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yMzA4MjkwNDIwMDNaFw0yMzA5MjgwNDIwMDNaMIGPMRMwEQYKCZIm +dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG @@ -18,11 +18,11 @@ F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg -Q29tIEluYy4gUm9vdCBDQYIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcN -AQELBQADggEBAIopqco/k9RSjouTeKP4z0EVUxdD4qnNh1GLSRqyAVe0aChyKF5f -qt1Bd1XCY8D16RgekkKGHDpJhGCpel+vtIoXPBxUaGQNYxmJCf5OzLMODlcrZk5i -jHIcv/FMeK02NBcz/WQ3mbWHVwXLhmwqa2zBsF4FmPCJAbFLchLhkAv1HJifHbnD -jQzlKyl5jxam/wtjWxSm0iyso0z2TgyzY+MESqjEqB1hZkCFzD1xtUOCxbXgtKae -dgfHVFuovr3fNLV3GvQk0s9okDwDUcqV7DSH61e5bUMfE84o3of8YA7+HUoPV5Du -8sTOKRf7ncGXdDRA8aofW268pTCuIu3+g/Y= +Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN +AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC +YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V +6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG +1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq +qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov +rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI= -----END CERTIFICATE----- diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml index b679ce24ce..04a36c49c1 100644 --- a/checkstyle/checkstyle.xml +++ b/checkstyle/checkstyle.xml @@ -121,6 +121,7 @@ + diff --git a/config/roles.yml b/config/roles.yml index 77906290a0..7b45deb813 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -33,6 +33,7 @@ alerting_read_access: - 'cluster:admin/opendistro/alerting/monitor/get' - 'cluster:admin/opendistro/alerting/monitor/search' - 'cluster:admin/opensearch/alerting/findings/get' + - 'cluster:admin/opensearch/alerting/remote/indexes/get' - 'cluster:admin/opensearch/alerting/workflow/get' - 'cluster:admin/opensearch/alerting/workflow_alerts/get' @@ -270,17 +271,40 @@ cross_cluster_search_remote_full_access: - 'indices:admin/shards/search_shards' - 'indices:data/read/search' +# Allow users to operate query assistant +query_assistant_access: + reserved: true + cluster_permissions: + - 'cluster:admin/opensearch/ml/config/get' + - 'cluster:admin/opensearch/ml/execute' + - 'cluster:admin/opensearch/ml/predict' + - 'cluster:admin/opensearch/ppl' + # Allow users to read ML stats/models/tasks ml_read_access: reserved: true cluster_permissions: + - 'cluster:admin/opensearch/ml/config/get' - 'cluster:admin/opensearch/ml/connectors/get' - 'cluster:admin/opensearch/ml/connectors/search' + - 'cluster:admin/opensearch/ml/controllers/get' + - 'cluster:admin/opensearch/ml/memory/conversation/get' + - 'cluster:admin/opensearch/ml/memory/conversation/interaction/search' + - 'cluster:admin/opensearch/ml/memory/conversation/list' + - 'cluster:admin/opensearch/ml/memory/conversation/search' + - 'cluster:admin/opensearch/ml/memory/interaction/get' + - 'cluster:admin/opensearch/ml/memory/interaction/list' + - 'cluster:admin/opensearch/ml/memory/trace/get' + - 'cluster:admin/opensearch/ml/model_groups/get' - 'cluster:admin/opensearch/ml/model_groups/search' - 'cluster:admin/opensearch/ml/models/get' - 'cluster:admin/opensearch/ml/models/search' + - 'cluster:admin/opensearch/ml/profile/nodes' + - 'cluster:admin/opensearch/ml/stats/nodes' - 'cluster:admin/opensearch/ml/tasks/get' - 'cluster:admin/opensearch/ml/tasks/search' + - 'cluster:admin/opensearch/ml/tools/get' + - 'cluster:admin/opensearch/ml/tools/list' # Allows users to use all ML functionality ml_full_access: @@ -346,6 +370,7 @@ security_analytics_read_access: - 'cluster:admin/opensearch/securityanalytics/detector/get' - 'cluster:admin/opensearch/securityanalytics/detector/search' - 'cluster:admin/opensearch/securityanalytics/findings/get' + - 'cluster:admin/opensearch/securityanalytics/logtype/search' - 'cluster:admin/opensearch/securityanalytics/mapping/get' - 'cluster:admin/opensearch/securityanalytics/mapping/view/get' - 'cluster:admin/opensearch/securityanalytics/rule/get' @@ -359,6 +384,7 @@ security_analytics_full_access: - 'cluster:admin/opensearch/securityanalytics/correlations/*' - 'cluster:admin/opensearch/securityanalytics/detector/*' - 'cluster:admin/opensearch/securityanalytics/findings/*' + - 'cluster:admin/opensearch/securityanalytics/logtype/*' - 'cluster:admin/opensearch/securityanalytics/mapping/*' - 'cluster:admin/opensearch/securityanalytics/rule/*' index_permissions: @@ -373,3 +399,38 @@ security_analytics_ack_alerts: reserved: true cluster_permissions: - 'cluster:admin/opensearch/securityanalytics/alerts/*' + +# Allows users to use all Flow Framework functionality +flow_framework_full_access: + reserved: true + cluster_permissions: + - 'cluster:admin/opensearch/flow_framework/*' + - 'cluster_monitor' + index_permissions: + - index_patterns: + - '*' + allowed_actions: + - 'indices:admin/aliases/get' + - 'indices:admin/mappings/get' + - 'indices_monitor' + +# Allow users to read flow framework's workflows and their state +flow_framework_read_access: + reserved: true + cluster_permissions: + - 'cluster:admin/opensearch/flow_framework/workflow/get' + - 'cluster:admin/opensearch/flow_framework/workflow/search' + - 'cluster:admin/opensearch/flow_framework/workflow_state/get' + - 'cluster:admin/opensearch/flow_framework/workflow_state/search' + - 'cluster:admin/opensearch/flow_framework/workflow_step/get' + +# Allows users to use all query insights APIs +query_insights_full_access: + reserved: true + cluster_permissions: + - 'cluster:admin/opensearch/insights/top_queries/*' + index_permissions: + - index_patterns: + - 'top_queries_by_*' + allowed_actions: + - "indices_all" diff --git a/gradle/formatting.gradle b/gradle/formatting.gradle index 2248c1d9a0..4fdea90277 100644 --- a/gradle/formatting.gradle +++ b/gradle/formatting.gradle @@ -17,6 +17,10 @@ allprojects { eclipse().configFile rootProject.file('formatter/formatterConfig.xml') trimTrailingWhitespace() endWithNewline(); + custom 'Replace illegal HttpStatus import w/ correct one', { + // e.g., replace org.apache.hc.core5.http.HttpStatus with org.apache.http.HttpStatus + it.replaceAll('org.apache.hc.core5.http.HttpStatus', 'org.apache.http.HttpStatus') + } // See DEVELOPER_GUIDE.md for details of when to enable this. if (System.getProperty('spotless.paddedcell') != null) { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 7f93135c49..d64cd49177 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 3999f7f3f6..ca8f2653b1 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae +distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 diff --git a/plugin-security.policy b/plugin-security.policy index 65b6b22fee..6a78a5cc91 100644 --- a/plugin-security.policy +++ b/plugin-security.policy @@ -34,6 +34,8 @@ grant { permission javax.security.auth.AuthPermission "modifyPrivateCredentials"; permission javax.security.auth.AuthPermission "doAs"; permission javax.security.auth.kerberos.ServicePermission "*","accept"; + + //SAML and internal plugin policy permission java.util.PropertyPermission "*","read,write"; //Enable when we switch to UnboundID LDAP SDK @@ -59,6 +61,8 @@ grant { permission java.security.SecurityPermission "putProviderProperty.BC"; permission java.security.SecurityPermission "insertProvider.BC"; permission java.security.SecurityPermission "removeProviderProperty.BC"; + permission java.security.SecurityPermission "getProperty.org.bouncycastle.ec.max_f2m_field_size"; + permission java.security.SecurityPermission "getProperty.org.bouncycastle.pkcs12.default"; permission java.security.SecurityPermission "getProperty.org.bouncycastle.rsa.max_size"; permission java.security.SecurityPermission "getProperty.org.bouncycastle.rsa.max_mr_tests"; @@ -74,8 +78,6 @@ grant { //Enable this permission to debug unauthorized de-serialization attempt //permission java.io.SerializablePermission "enableSubstitution"; - //SAML policy - permission java.util.PropertyPermission "*", "read,write"; }; grant codeBase "${codebase.netty-common}" { diff --git a/release-notes/opensearch-security.release-notes-2.11.1.0.md b/release-notes/opensearch-security.release-notes-2.11.1.0.md new file mode 100644 index 0000000000..0164f2819e --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.11.1.0.md @@ -0,0 +1,7 @@ +## 2023-11-21 Version 2.11.1.0 + +Compatible with OpenSearch 2.11.1 + +### Bug Fixes +* Fix regression on concurrent gzipped requests ([#3599](https://github.com/opensearch-project/security/pull/3599)) +* Fix issue with response content-types changed in 2.11 ([#3721](https://github.com/opensearch-project/security/pull/3721)) diff --git a/release-notes/opensearch-security.release-notes-2.12.0.0.md b/release-notes/opensearch-security.release-notes-2.12.0.0.md new file mode 100644 index 0000000000..be6ddbc125 --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.12.0.0.md @@ -0,0 +1,61 @@ +## 2024-02-20 Version 2.12.0.0 + +Compatible with OpenSearch 2.12.0 + +### Enhancements +* Add additional sendRequestDecorate cases ([#4007](https://github.com/opensearch-project/security/pull/4007)) +* [BUG-2556] Add new DLS filtering test ([#4001](https://github.com/opensearch-project/security/pull/4001)) +* [Enhancement-3191] `transport_enabled` setting on an auth domain and authorizer may be unnecessary after transport client removal ([#3966](https://github.com/opensearch-project/security/pull/3966)) +* Update roles.yml with new API for experimental alerting plugin feature [#4027](https://github.com/opensearch-project/security/pull/4027) ([#4029](https://github.com/opensearch-project/security/pull/4029)) +* Admin role for Query insights plugin ([#4022](https://github.com/opensearch-project/security/pull/4022)) +* Validate 409s occur when multiple config updates happen simultaneously ([#3962](https://github.com/opensearch-project/security/pull/3962)) +* Protect config object from concurrent modification issues ([#3956](https://github.com/opensearch-project/security/pull/3956)) +* Add test coverage for ComplianceConfig ([#3957](https://github.com/opensearch-project/security/pull/3957)) +* Update security analytics roles to include custom log type cluster permissions ([#3954](https://github.com/opensearch-project/security/pull/3954)) +* Add logging for test LdapServer actions ([#3942](https://github.com/opensearch-project/security/pull/3942)) +* HeapBasedRateTracker uses time provider to allow simluating of time in unit tests ([#3941](https://github.com/opensearch-project/security/pull/3941)) +* Add additional logging around `testShouldSearchAll` tests ([#3943](https://github.com/opensearch-project/security/pull/3943)) +* Add permission for get workflow step ([#3940](https://github.com/opensearch-project/security/pull/3940)) +* Add additional ignore_headers audit configuration setting ([#3926](https://github.com/opensearch-project/security/pull/3926)) +* Update to Gradle 8.5 ([#3919](https://github.com/opensearch-project/security/pull/3919)) ([#3923](https://github.com/opensearch-project/security/pull/3923)) +* Refactor SSL handler retrieval to use HttpChannel / TranportChannel APIs instead of typecasting ([#3917](https://github.com/opensearch-project/security/pull/3917)) ([#3922](https://github.com/opensearch-project/security/pull/3922)) +* Improve messaging on how to set initial admin password ([#3918](https://github.com/opensearch-project/security/pull/3918)) +* Re-enable disabled PIT integration tests ([#3914](https://github.com/opensearch-project/security/pull/3914)) +* Switched to more reliable OpenSearch Lucene snapshot location ([#3913](https://github.com/opensearch-project/security/pull/3913)) +* Add deprecation check for `jwt_header` setting ([#3896](https://github.com/opensearch-project/security/pull/3896)) +* Add render search template as a cluster permission ([#3689](https://github.com/opensearch-project/security/pull/3689)) ([#3872](https://github.com/opensearch-project/security/pull/3872)) +* Add flow framework system indices and roles ([#3851](https://github.com/opensearch-project/security/pull/3851)) ([#3880](https://github.com/opensearch-project/security/pull/3880)) +* Search operation test flakiness fix ([#3862](https://github.com/opensearch-project/security/pull/3862)) +* Extracts demo configuration setup into a java tool, adds support for Bundled JDK for this tool and updates DEVELOPER_GUIDE.md ([#3845](https://github.com/opensearch-project/security/pull/3845)) +* SAML permissions changes in DynamicConfigModelV7 ([#3853](https://github.com/opensearch-project/security/pull/3853)) +* Add do not fail on forbidden test cases around the stats API ([#3825](https://github.com/opensearch-project/security/pull/3825)) ([#3828](https://github.com/opensearch-project/security/pull/3828)) + +### Bug Fixes +* Fix Bug with Install demo configuration running in cluster mode with -y ([#3936](https://github.com/opensearch-project/security/pull/3936)) +* Allow TransportConfigUpdateAction when security config initialization has completed ([#3810](https://github.com/opensearch-project/security/pull/3810)) ([#3927](https://github.com/opensearch-project/security/pull/3927)) +* Fix the CI / report-coverage check by switching to corresponding actions/upload-artifact@v4 ([#3893](https://github.com/opensearch-project/security/pull/3893)) ([#3895](https://github.com/opensearch-project/security/pull/3895)) + +### Maintenance +* Bump org.apache.camel:camel-xmlsecurity from 3.22.0 to 3.22.1 ([#4018](https://github.com/opensearch-project/security/pull/4018)) +* Bump release-drafter/release-drafter from 5 to 6 ([#4021](https://github.com/opensearch-project/security/pull/4021)) +* Bump com.netflix.nebula.ospackage from 11.6.0 to 11.7.0 ([#4019](https://github.com/opensearch-project/security/pull/4019)) +* Bump org.junit.jupiter:junit-jupiter from 5.10.1 to 5.10.2 ([#4020](https://github.com/opensearch-project/security/pull/4020)) +* Bump jjwt_version from 0.12.4 to 0.12.5 ([#4017](https://github.com/opensearch-project/security/pull/4017)) +* Bump io.dropwizard.metrics:metrics-core from 4.2.24 to 4.2.25 ([#3998](https://github.com/opensearch-project/security/pull/3998)) +* Bump gradle/gradle-build-action from 2 to 3 ([#4000](https://github.com/opensearch-project/security/pull/4000)) +* Bump jjwt_version from 0.12.3 to 0.12.4 ([#3999](https://github.com/opensearch-project/security/pull/3999)) +* Bump spotless (6.24.0 -> 6.25.0) to bump eclipse resources (3.18 -> 3.19) ([#3993](https://github.com/opensearch-project/security/pull/3993)) +* Fix: remove unnecessary trailing slashes in APIs. ([#3978](https://github.com/opensearch-project/security/pull/3978)) +* Adds new ml-commons system indices to the list ([#3974](https://github.com/opensearch-project/security/pull/3974)) +* Bump io.dropwizard.metrics:metrics-core from 4.2.23 to 4.2.24 ([#3970](https://github.com/opensearch-project/security/pull/3970)) +* Bump com.fasterxml.woodstox:woodstox-core from 6.5.1 to 6.6.0 ([#3969](https://github.com/opensearch-project/security/pull/3969)) +* Bump com.diffplug.spotless from 6.23.3 to 6.24.0 ([#3947](https://github.com/opensearch-project/security/pull/3947)) +* Bump org.apache.camel:camel-xmlsecurity from 3.21.3 to 3.22.0 ([#3906](https://github.com/opensearch-project/security/pull/3906)) +* Bump com.google.errorprone:error_prone_annotations from 2.23.0 to 2.24.0 ([#3897](https://github.com/opensearch-project/security/pull/3897)) ([#3902](https://github.com/opensearch-project/security/pull/3902)) +* Bump io.dropwizard.metrics:metrics-core from 4.2.22 to 4.2.23 ([#3900](https://github.com/opensearch-project/security/pull/3900)) +* Bump com.google.googlejavaformat:google-java-format from 1.18.1 to 1.19.1 ([#3901](https://github.com/opensearch-project/security/pull/3901)) +* Bump github/codeql-action from 2 to 3 ([#3859](https://github.com/opensearch-project/security/pull/3859)) ([#3867](https://github.com/opensearch-project/security/pull/3867)) +* Bump org.apache.camel:camel-xmlsecurity from 3.21.2 to 3.21.3 ([#3864](https://github.com/opensearch-project/security/pull/3864)) +* Bump org.checkerframework:checker-qual from 3.40.0 to 3.42.0 ([#3857](https://github.com/opensearch-project/security/pull/3857)) ([#3866](https://github.com/opensearch-project/security/pull/3866)) +* Bump com.flipkart.zjsonpatch:zjsonpatch from 0.4.14 to 0.4.16 ([#3865](https://github.com/opensearch-project/security/pull/3865)) +* Bump com.netflix.nebula.ospackage from 11.5.0 to 11.6.0 ([#3863](https://github.com/opensearch-project/security/pull/3863)) diff --git a/release-notes/opensearch-security.release-notes-2.13.0.0.md b/release-notes/opensearch-security.release-notes-2.13.0.0.md new file mode 100644 index 0000000000..48ecfa9b87 --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.13.0.0.md @@ -0,0 +1,30 @@ +## Version 2.13.0.0 + +Compatible with OpenSearch 2.13.0 + +### Enhancements +* Admin role for Query insights plugin ([#4022](https://github.com/opensearch-project/security/pull/4022)) +* Add query assistant role and new ml system indices ([#4143](https://github.com/opensearch-project/security/pull/4143)) +* Redact sensitive configuration values when retrieving security configuration ([#4028](https://github.com/opensearch-project/security/pull/4028)) +* v2.12 update roles.yml with new API for experimental alerting plugin feature ([#4035](https://github.com/opensearch-project/security/pull/4035)) +* Add deprecate message that TLSv1 and TLSv1.1 support will be removed in the next major version ([#4083](https://github.com/opensearch-project/security/pull/4083)) +* Log password requirement details in demo environment ([#4082](https://github.com/opensearch-project/security/pull/4082)) +* Redact sensitive URL parameters from audit logging ([#4070](https://github.com/opensearch-project/security/pull/4070)) +* Fix unconsumed parameter exception when authenticating with jwtUrlParameter ([#4065](https://github.com/opensearch-project/security/pull/4065)) +* Regenerates root-ca, kirk and esnode certificates to address already expired root ca certificate ([#4066](https://github.com/opensearch-project/security/pull/4066)) +* Add exclude_roles configuration parameter to LDAP authorization backend ([#4043](https://github.com/opensearch-project/security/pull/4043)) + +### Maintenance +* Add exlusion for logback-core to resolve CVE-2023-6378 ([#4050](https://github.com/opensearch-project/security/pull/4050)) +* Bump com.netflix.nebula.ospackage from 11.7.0 to 11.8.1 ([#4041](https://github.com/opensearch-project/security/pull/4041), [#4075](https://github.com/opensearch-project/security/pull/4075)) +* Bump Wandalen/wretry.action from 1.3.0 to 1.4.10 ([#4042](https://github.com/opensearch-project/security/pull/4042), [#4092](https://github.com/opensearch-project/security/pull/4092), [#4108](https://github.com/opensearch-project/security/pull/4108), [#4135](https://github.com/opensearch-project/security/pull/4135)) +* Bump spring_version from 5.3.31 to 5.3.33 ([#4058](https://github.com/opensearch-project/security/pull/4058), [#4131](https://github.com/opensearch-project/security/pull/4131)) +* Bump org.scala-lang:scala-library from 2.13.12 to 2.13.13 ([#4076](https://github.com/opensearch-project/security/pull/4076)) +* Bump com.google.googlejavaformat:google-java-format from 1.19.1 to 1.21.0 ([#4078](https://github.com/opensearch-project/security/pull/4078), [#4110](https://github.com/opensearch-project/security/pull/4110)) +* Bump ch.qos.logback:logback-classic from 1.2.13 to 1.5.3 ([#4091](https://github.com/opensearch-project/security/pull/4091), [#4111](https://github.com/opensearch-project/security/pull/4111)) +* Bump com.fasterxml.woodstox:woodstox-core from 6.6.0 to 6.6.1 ([#4093](https://github.com/opensearch-project/security/pull/4093)) +* Bump kafka_version from 3.5.1 to 3.7.0 ([#4095](https://github.com/opensearch-project/security/pull/4095)) +* Bump jakarta.xml.bind:jakarta.xml.bind-api from 4.0.1 to 4.0.2 ([#4109](https://github.com/opensearch-project/security/pull/4109)) +* Bump org.apache.zookeeper:zookeeper from 3.9.1. to 3.9.2 ([#4130](https://github.com/opensearch-project/security/pull/4130)) +* Bump org.awaitility:awaitility from 4.2.0 to 4.2.1 ([#4133](https://github.com/opensearch-project/security/pull/4133)) +* Bump com.google.errorprone:error_prone_annotations from 2.25.0 to 2.26.1 ([#4132](https://github.com/opensearch-project/security/pull/4132)) diff --git a/release-notes/opensearch-security.release-notes-2.14.0.0.md b/release-notes/opensearch-security.release-notes-2.14.0.0.md new file mode 100644 index 0000000000..2d089866f9 --- /dev/null +++ b/release-notes/opensearch-security.release-notes-2.14.0.0.md @@ -0,0 +1,39 @@ +## Version 2.14.0.0 + +Compatible with OpenSearch 2.14.0 + +### Enhancements +* Check for and perform upgrades on security configurations ([#4251](https://github.com/opensearch-project/security/pull/4251)) +* Replace bouncy castle blake2b ([#4284](https://github.com/opensearch-project/security/pull/4284)) +* Adds saml auth header to differentiate saml requests and prevents auto login as anonymous user when basic authentication fails ([#4228](https://github.com/opensearch-project/security/pull/4228)) +* Dynamic sign in options ([#4137](https://github.com/opensearch-project/security/pull/4137)) +* Add index permissions for query insights exporters ([#4231](https://github.com/opensearch-project/security/pull/4231)) +* Add new stop words system index ([#4181](https://github.com/opensearch-project/security/pull/4181)) +* Switch to built-in security transports from core ([#4119](https://github.com/opensearch-project/security/pull/4119)) ([#4174](https://github.com/opensearch-project/security/pull/4174)) ([#4187](https://github.com/opensearch-project/security/pull/4187)) +* System index permission grants reading access to documents in the index ([#4291](https://github.com/opensearch-project/security/pull/4291)) +* Improve cluster initialization reliability ([#4002](https://github.com/opensearch-project/security/pull/4002)) ([#4256](https://github.com/opensearch-project/security/pull/4256)) + +### Bug Fixes +* Ensure that challenge response contains body ([#4268](https://github.com/opensearch-project/security/pull/4268)) +* Add logging for audit log that are unable to saving the request body ([#4272](https://github.com/opensearch-project/security/pull/4272)) +* Use predictable serialization logic for transport headers ([#4288](https://github.com/opensearch-project/security/pull/4288)) +* Update Log4JSink Default from sgaudit to audit and add test for default values ([#4155](https://github.com/opensearch-project/security/pull/4155)) +* Remove Pom task dependencies rewrite ([#4178](https://github.com/opensearch-project/security/pull/4178)) ([#4186](https://github.com/opensearch-project/security/pull/4186)) +* Misc changes for tests ([#4184](https://github.com/opensearch-project/security/pull/4184)) +* Add simple roles mapping integ test to test mapping of backend role to role ([#4176](https://github.com/opensearch-project/security/pull/4176)) + +### Maintenance +* Add getProperty.org.bouncycastle.ec.max_f2m_field_size to plugin-security.policy ([#4270](https://github.com/opensearch-project/security/pull/4270)) +* Add getProperty.org.bouncycastle.pkcs12.default to plugin-security.policy ([#4266](https://github.com/opensearch-project/security/pull/4266)) +* Bump apache_cxf_version from 4.0.3 to 4.0.4 ([#4287](https://github.com/opensearch-project/security/pull/4287)) +* Bump ch.qos.logback:logback-classic from 1.5.3 to 1.5.5 ([#4248](https://github.com/opensearch-project/security/pull/4248)) +* Bump codecov/codecov-action from v3 to v4 ([#4237](https://github.com/opensearch-project/security/pull/4237)) +* Bump com.fasterxml.woodstox:woodstox-core from 6.6.1 to 6.6.2 ([#4195](https://github.com/opensearch-project/security/pull/4195)) +* Bump com.google.googlejavaformat:google-java-format from 1.21.0 to 1.22.0 ([#4220](https://github.com/opensearch-project/security/pull/4220)) +* Bump commons-io:commons-io from 2.15.1 to 2.16.1 ([#4196](https://github.com/opensearch-project/security/pull/4196)) ([#4246](https://github.com/opensearch-project/security/pull/4246)) +* Bump com.nulab-inc:zxcvbn from 1.8.2 to 1.9.0 ([#4219](https://github.com/opensearch-project/security/pull/4219)) +* Bump io.dropwizard.metrics:metrics-core from 4.2.15 to 4.2.25 ([#4193](https://github.com/opensearch-project/security/pull/4193)) ([#4197](https://github.com/opensearch-project/security/pull/4197)) +* Bump net.shibboleth.utilities:java-support from 8.4.1 to 8.4.2 ([#4245](https://github.com/opensearch-project/security/pull/4245)) +* Bump spring_version from 5.3.33 to 5.3.34 ([#4250](https://github.com/opensearch-project/security/pull/4250)) +* Bump Wandalen/wretry.action from 1.4.10 to 3.3.0 ([#4167](https://github.com/opensearch-project/security/pull/4167)) ([#4198](https://github.com/opensearch-project/security/pull/4198)) ([#4221](https://github.com/opensearch-project/security/pull/4221)) ([#4247](https://github.com/opensearch-project/security/pull/4247)) +* Bump open_saml_version from 4.3.0 to 4.3.2 ([#4303](https://github.com/opensearch-project/security/pull/4303)) ([#4239](https://github.com/opensearch-project/security/pull/4239)) diff --git a/scripts/integtest.sh b/scripts/integtest.sh deleted file mode 100755 index 98ee40fbd6..0000000000 --- a/scripts/integtest.sh +++ /dev/null @@ -1,105 +0,0 @@ -#!/bin/bash - -set -e - -function usage() { - echo "" - echo "This script is used to run integration tests for plugin installed on a remote OpenSearch/Dashboards cluster." - echo "--------------------------------------------------------------------------" - echo "Usage: $0 [args]" - echo "" - echo "Required arguments:" - echo "None" - echo "" - echo "Optional arguments:" - echo -e "-b BIND_ADDRESS\t, defaults to localhost | 127.0.0.1, can be changed to any IP or domain name for the cluster location." - echo -e "-p BIND_PORT\t, defaults to 9200, can be changed to any port for the cluster location." - echo -e "-s SECURITY_ENABLED\t(true | false), defaults to true. Specify the OpenSearch/Dashboards have security enabled or not." - echo -e "-c CREDENTIAL\t(usename:password), no defaults, effective when SECURITY_ENABLED=true." - echo -e "-h\tPrint this message." - echo -e "-v OPENSEARCH_VERSION\t, no defaults" - echo -e "-n SNAPSHOT\t, defaults to false" - echo -e "-m CLUSTER_NAME\t, defaults to docker-cluster" - echo "--------------------------------------------------------------------------" -} - -while getopts ":h:b:p:s:c:v:n:t:m:u:" arg; do - case $arg in - h) - usage - exit 1 - ;; - b) - BIND_ADDRESS=$OPTARG - ;; - p) - BIND_PORT=$OPTARG - ;; - t) - TRANSPORT_PORT=$OPTARG - ;; - s) - SECURITY_ENABLED=$OPTARG - ;; - c) - CREDENTIAL=$OPTARG - ;; - m) - CLUSTER_NAME=$OPTARG - ;; - v) - # Do nothing as we're not consuming this param. - ;; - n) - # Do nothing as we're not consuming this param. - ;; - u) - COMMON_UTILS_VERSION=$OPTARG - ;; - :) - echo "-${OPTARG} requires an argument" - usage - exit 1 - ;; - ?) - echo "Invalid option: -${OPTARG}" - exit 1 - ;; - esac -done - - -if [ -z "$BIND_ADDRESS" ] -then - BIND_ADDRESS="localhost" -fi - -if [ -z "$BIND_PORT" ] -then - BIND_PORT="9200" -fi - -if [ -z "$SECURITY_ENABLED" ] -then - SECURITY_ENABLED="true" -fi - -if [ -z "$CREDENTIAL" ] -then - CREDENTIAL="admin:admin" -fi - -if [ -z "$CREDENTIAL" ] -then - CREDENTIAL="admin:admin" -fi - -if [ -z "$CLUSTER_NAME" ] -then - CLUSTER_NAME="docker-cluster" -fi - -USERNAME=`echo $CREDENTIAL | awk -F ':' '{print $1}'` -PASSWORD=`echo $CREDENTIAL | awk -F ':' '{print $2}'` - -./gradlew integTestRemote -Dtests.rest.cluster="$BIND_ADDRESS:$BIND_PORT" -Dtests.cluster="$BIND_ADDRESS:$BIND_PORT" -Dsecurity_enabled=$SECURITY_ENABLED -Dtests.clustername=$CLUSTER_NAME -Dhttps=true -Duser=$USERNAME -Dpassword=$PASSWORD diff --git a/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java new file mode 100644 index 0000000000..5387b3e516 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/AbstractDefaultConfigurationTests.java @@ -0,0 +1,180 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.fasterxml.jackson.databind.JsonNode; +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.security.state.SecurityMetadata; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.aMapWithSize; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public abstract class AbstractDefaultConfigurationTests { + public final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin"); + private static final TestSecurityConfig.User NEW_USER = new TestSecurityConfig.User("new-user"); + private static final TestSecurityConfig.User LIMITED_USER = new TestSecurityConfig.User("limited-user"); + + private final LocalCluster cluster; + + protected AbstractDefaultConfigurationTests(LocalCluster cluster) { + this.cluster = cluster; + } + + @AfterClass + public static void cleanConfigurationDirectory() throws IOException { + FileUtils.deleteDirectory(configurationFolder.toFile()); + } + + @Test + public void shouldLoadDefaultConfiguration() { + try (TestRestClient client = cluster.getRestClient(NEW_USER)) { + Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + } + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.confirmCorrectCredentials(ADMIN_USER.getName()); + TestRestClient.HttpResponse response = client.get("_plugins/_security/api/internalusers"); + response.assertStatusCode(HttpStatus.SC_OK); + Map users = response.getBodyAs(Map.class); + assertThat( + response.getBody(), + users, + allOf(aMapWithSize(3), hasKey(ADMIN_USER.getName()), hasKey(NEW_USER.getName()), hasKey(LIMITED_USER.getName())) + ); + } + } + + void assertClusterState(final TestRestClient client) { + if (cluster.node().settings().getAsBoolean("plugins.security.allow_default_init_securityindex.use_cluster_state", false)) { + final TestRestClient.HttpResponse response = client.get("_cluster/state"); + response.assertStatusCode(HttpStatus.SC_OK); + final var clusterState = response.getBodyAs(Map.class); + assertTrue(response.getBody(), clusterState.containsKey(SecurityMetadata.TYPE)); + @SuppressWarnings("unchecked") + final var securityClusterState = (Map) clusterState.get(SecurityMetadata.TYPE); + @SuppressWarnings("unchecked") + final var securityConfiguration = (Map) ((Map) clusterState.get(SecurityMetadata.TYPE)).get( + "configuration" + ); + assertTrue(response.getBody(), securityClusterState.containsKey("created")); + assertNotNull(response.getBody(), securityClusterState.get("created")); + + for (final var k : securityConfiguration.keySet()) { + @SuppressWarnings("unchecked") + final var sc = (Map) securityConfiguration.get(k); + assertTrue(response.getBody(), sc.containsKey("hash")); + assertTrue(response.getBody(), sc.containsKey("last_modified")); + } + } + } + + @Test + public void securityRolesUpgrade() throws Exception { + try (var client = cluster.getRestClient(ADMIN_USER)) { + // Setup: Make sure the config is ready before starting modifications + Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + + // Setup: Collect default roles after cluster start + final var expectedRoles = client.get("_plugins/_security/api/roles/"); + final var expectedRoleNames = extractFieldNames(expectedRoles.getBodyAs(JsonNode.class)); + + // Verify: Before any changes, nothing to upgrade + final var upgradeCheck = client.get("_plugins/_security/api/_upgrade_check"); + upgradeCheck.assertStatusCode(200); + assertThat(upgradeCheck.getBooleanFromJsonBody("/upgradeAvailable"), equalTo(false)); + + // Action: Select a role that is part of the defaults and delete that role + final var roleToDelete = "flow_framework_full_access"; + client.delete("_plugins/_security/api/roles/" + roleToDelete).assertStatusCode(200); + + // Action: Select a role that is part of the defaults and alter that role with removal, edits, and additions + final var roleToAlter = "flow_framework_read_access"; + final var originalRoleConfig = client.get("_plugins/_security/api/roles/" + roleToAlter).getBodyAs(JsonNode.class); + final var alteredRoleReponse = client.patch("_plugins/_security/api/roles/" + roleToAlter, "[\n" + // + " {\n" + // + " \"op\": \"replace\",\n" + // + " \"path\": \"/cluster_permissions\",\n" + // + " \"value\": [\"a\", \"b\", \"c\"]\n" + // + " },\n" + // + " {\n" + // + " \"op\": \"add\",\n" + // + " \"path\": \"/index_permissions\",\n" + // + " \"value\": [ {\n" + // + " \"index_patterns\": [\"*\"],\n" + // + " \"allowed_actions\": [\"*\"]\n" + // + " }\n" + // + " ]\n" + // + " }\n" + // + "]"); + alteredRoleReponse.assertStatusCode(200); + final var alteredRoleJson = alteredRoleReponse.getBodyAs(JsonNode.class); + assertThat(originalRoleConfig, not(equalTo(alteredRoleJson))); + + // Verify: Confirm that the upgrade check detects the changes associated with both role resources + final var upgradeCheckAfterChanges = client.get("_plugins/_security/api/_upgrade_check"); + upgradeCheckAfterChanges.assertStatusCode(200); + assertThat( + upgradeCheckAfterChanges.getTextArrayFromJsonBody("/upgradeActions/roles/add"), + equalTo(List.of("flow_framework_full_access")) + ); + assertThat( + upgradeCheckAfterChanges.getTextArrayFromJsonBody("/upgradeActions/roles/modify"), + equalTo(List.of("flow_framework_read_access")) + ); + + // Action: Perform the upgrade to the roles configuration + final var performUpgrade = client.post("_plugins/_security/api/_upgrade_perform"); + performUpgrade.assertStatusCode(200); + assertThat(performUpgrade.getTextArrayFromJsonBody("/upgrades/roles/add"), equalTo(List.of("flow_framework_full_access"))); + assertThat(performUpgrade.getTextArrayFromJsonBody("/upgrades/roles/modify"), equalTo(List.of("flow_framework_read_access"))); + + // Verify: Same roles as the original state - the deleted role has been restored + final var afterUpgradeRoles = client.get("_plugins/_security/api/roles/"); + final var afterUpgradeRolesNames = extractFieldNames(afterUpgradeRoles.getBodyAs(JsonNode.class)); + assertThat(afterUpgradeRolesNames, equalTo(expectedRoleNames)); + + // Verify: Altered role was restored to its expected state + final var afterUpgradeAlteredRoleConfig = client.get("_plugins/_security/api/roles/" + roleToAlter).getBodyAs(JsonNode.class); + assertThat(originalRoleConfig, equalTo(afterUpgradeAlteredRoleConfig)); + } + } + + private Set extractFieldNames(final JsonNode json) { + final var set = new HashSet(); + json.fieldNames().forEachRemaining(set::add); + return set; + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java index 287bc139b1..04f2892eeb 100644 --- a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java +++ b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java @@ -9,37 +9,34 @@ */ package org.opensearch.security; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.StandardOpenOption; import java.util.Objects; -class ConfigurationFiles { +import org.opensearch.core.common.Strings; +import org.opensearch.security.securityconf.impl.CType; - public static void createRoleMappingFile(File destination) { - String resource = "roles_mapping.yml"; - copyResourceToFile(resource, destination); - } +public class ConfigurationFiles { public static Path createConfigurationDirectory() { try { Path tempDirectory = Files.createTempDirectory("test-security-config"); String[] configurationFiles = { - "config.yml", - "action_groups.yml", - "config.yml", - "internal_users.yml", - "roles.yml", - "roles_mapping.yml", + CType.ACTIONGROUPS.configFileName(), + CType.CONFIG.configFileName(), + CType.INTERNALUSERS.configFileName(), + CType.NODESDN.configFileName(), + CType.ROLES.configFileName(), + CType.ROLESMAPPING.configFileName(), "security_tenants.yml", - "tenants.yml" }; + CType.TENANTS.configFileName(), + CType.WHITELIST.configFileName() }; for (String fileName : configurationFiles) { - Path configFileDestination = tempDirectory.resolve(fileName); - copyResourceToFile(fileName, configFileDestination.toFile()); + copyResourceToFile(fileName, tempDirectory.resolve(fileName)); } return tempDirectory.toAbsolutePath(); } catch (IOException ex) { @@ -47,10 +44,18 @@ public static Path createConfigurationDirectory() { } } - private static void copyResourceToFile(String resource, File destination) { + public static void writeToConfig(final CType cType, final Path configFolder, final String content) throws IOException { + if (Strings.isNullOrEmpty(content)) return; + try (final var out = Files.newOutputStream(cType.configFile(configFolder), StandardOpenOption.APPEND)) { + out.write(content.getBytes(StandardCharsets.UTF_8)); + out.flush(); + } + } + + public static void copyResourceToFile(String resource, Path destination) { try (InputStream input = ConfigurationFiles.class.getClassLoader().getResourceAsStream(resource)) { Objects.requireNonNull(input, "Cannot find source resource " + resource); - try (OutputStream output = new FileOutputStream(destination)) { + try (final var output = Files.newOutputStream(destination)) { input.transferTo(output); } } catch (IOException e) { diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java new file mode 100644 index 0000000000..704e2c7255 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterTests.java @@ -0,0 +1,39 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationMultiNodeClusterTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationMultiNodeClusterTests() { + super(cluster); + } +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java new file mode 100644 index 0000000000..8abffac9cf --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationMultiNodeClusterUseClusterStateTests.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationMultiNodeClusterUseClusterStateTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.allow_default_init_securityindex.use_cluster_state", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationMultiNodeClusterUseClusterStateTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java new file mode 100644 index 0000000000..362245db5e --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterTests.java @@ -0,0 +1,44 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class DefaultConfigurationSingleNodeClusterTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationSingleNodeClusterTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java new file mode 100644 index 0000000000..e05005e912 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationSingleNodeClusterUseClusterStateTests.java @@ -0,0 +1,42 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.util.List; +import java.util.Map; + +import org.junit.ClassRule; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; + +public class DefaultConfigurationSingleNodeClusterUseClusterStateTests extends AbstractDefaultConfigurationTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.allow_default_init_securityindex.use_cluster_state", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); + + public DefaultConfigurationSingleNodeClusterUseClusterStateTests() { + super(cluster); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java deleted file mode 100644 index 043d3908e9..0000000000 --- a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java +++ /dev/null @@ -1,78 +0,0 @@ -/* -* Copyright OpenSearch Contributors -* SPDX-License-Identifier: Apache-2.0 -* -* The OpenSearch Contributors require contributions made to -* this file be licensed under the Apache-2.0 license or a -* compatible open source license. -* -*/ -package org.opensearch.security; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.List; -import java.util.Map; - -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.commons.io.FileUtils; -import org.awaitility.Awaitility; -import org.junit.AfterClass; -import org.junit.ClassRule; -import org.junit.Test; -import org.junit.runner.RunWith; - -import org.opensearch.test.framework.cluster.ClusterManager; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; -import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.aMapWithSize; -import static org.hamcrest.Matchers.allOf; -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasKey; - -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class DefaultConfigurationTests { - - private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); - public static final String ADMIN_USER_NAME = "admin"; - public static final String DEFAULT_PASSWORD = "secret"; - public static final String NEW_USER = "new-user"; - public static final String LIMITED_USER = "limited-user"; - - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) - .nodeSettings( - Map.of( - "plugins.security.allow_default_init_securityindex", - true, - "plugins.security.restapi.roles_enabled", - List.of("user_admin__all_access") - ) - ) - .defaultConfigurationInitDirectory(configurationFolder.toString()) - .loadConfigurationIntoIndex(false) - .build(); - - @AfterClass - public static void cleanConfigurationDirectory() throws IOException { - FileUtils.deleteDirectory(configurationFolder.toFile()); - } - - @Test - public void shouldLoadDefaultConfiguration() { - try (TestRestClient client = cluster.getRestClient(NEW_USER, DEFAULT_PASSWORD)) { - Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); - } - try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { - client.confirmCorrectCredentials(ADMIN_USER_NAME); - HttpResponse response = client.get("/_plugins/_security/api/internalusers"); - response.assertStatusCode(200); - Map users = response.getBodyAs(Map.class); - assertThat(users, allOf(aMapWithSize(3), hasKey(ADMIN_USER_NAME), hasKey(NEW_USER), hasKey(LIMITED_USER))); - } - } -} diff --git a/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java b/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java index d1957e50a6..3e3ac61502 100644 --- a/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java +++ b/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java @@ -10,12 +10,15 @@ package org.opensearch.security; import java.io.IOException; +import java.io.Serializable; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.function.BiFunction; +import java.util.stream.Collectors; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.commons.lang3.tuple.Pair; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Test; @@ -36,6 +39,7 @@ import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.ADD; import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.opensearch.client.RequestOptions.DEFAULT; @@ -52,10 +56,12 @@ import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; import static org.opensearch.test.framework.cluster.SearchRequestFactory.averageAggregationRequest; +import static org.opensearch.test.framework.cluster.SearchRequestFactory.searchRequestWithSort; import static org.opensearch.test.framework.matcher.SearchResponseMatchers.containAggregationWithNameAndType; import static org.opensearch.test.framework.matcher.SearchResponseMatchers.isSuccessfulSearchResponse; import static org.opensearch.test.framework.matcher.SearchResponseMatchers.numberOfTotalHitsIsEqualTo; import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitContainsFieldWithValue; +import static org.opensearch.test.framework.matcher.SearchResponseMatchers.searchHitsContainDocumentsInAnyOrder; @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) @@ -81,6 +87,7 @@ public class DlsIntegrationTests { static final String FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-twins-artist"); static final String FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-first-artist"); static final String ALL_INDICES_ALIAS = "_all"; + static final String UNION_TEST_INDEX_NAME = "my_index1"; static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); @@ -157,6 +164,62 @@ public class DlsIntegrationTests { .on("*") ); + /** + * Test role 1 for DLS filtering with two (non)overlapping roles. This role imposes a filter where the user can only access documents where the sensitive field is false. This role is applied at a higher level for all index patterns. + */ + static final TestSecurityConfig.Role ROLE_NON_SENSITIVE_ONLY = new TestSecurityConfig.Role("test_role_1").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").dls("{\"match\":{\"sensitive\":false}}").on("*"); + + /** + * Test role 2 for DLS filtering with two overlapping roles. This role does not impose any filter, and combined with TEST_ROLE_ONE should yield a union that does not impose any filter. This role is applied at a lower level for index patterns my_index*. + */ + static final TestSecurityConfig.Role ROLE_ALLOW_ALL = new TestSecurityConfig.Role("test_role_2").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").dls("{\"match_all\": {}}").on("my_index*"); + + /** + * Test role 3 for DLS filtering with two nonoverlapping roles. This role imposes a filter where the user can only access documents where the genre field is History, and combined with TEST_ROLE_ONE should yield a union that allows the user to access every document except the one with genre Science and sensitive true. This role is applied at a lower level for index patterns my_index*. + */ + static final TestSecurityConfig.Role ROLE_MATCH_HISTORY_GENRE_ONLY = new TestSecurityConfig.Role("test_role_3").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").dls("{\"match\":{\"genre\":\"History\"}}").on("my_index*"); + + /** + * User with DLS permission to only be able to access documents with false sensitive property. + */ + static final TestSecurityConfig.User USER_NON_SENSITIVE_ONLY = new TestSecurityConfig.User("test_role_1_user").roles( + ROLE_NON_SENSITIVE_ONLY + ); + + /** + * User with DLS permission to access all documents. + */ + static final TestSecurityConfig.User USER_ALLOW_ALL = new TestSecurityConfig.User("test_role_2_user").roles(ROLE_ALLOW_ALL); + + /** + * User with DLS permission to access documents with genre property matching History. + */ + static final TestSecurityConfig.User USER_MATCH_HISTORY_GENRE_ONLY = new TestSecurityConfig.User("test_role_3_user").roles( + ROLE_MATCH_HISTORY_GENRE_ONLY + ); + + /** + * User with overlapping DLS permissions to access documents with false sensitive property and access all documents- should yield accessing all documents. + */ + static final TestSecurityConfig.User USER_UNION_OF_OVERLAPPING_ROLES_NON_SENSITIVE_ONLY_AND_ALLOW_ALL = new TestSecurityConfig.User( + "test_union_of_overlapping_roles_user" + ).roles(ROLE_NON_SENSITIVE_ONLY, ROLE_ALLOW_ALL); + + /** + * User with non-overlapping DLS permissions to access documents with false sensitive property and genre property matching History. + */ + static final TestSecurityConfig.User USER_UNION_OF_NONOVERLAPPING_ROLES_NON_SENSITIVE_ONLY_AND_HISTORY_GENRE_ONLY = + new TestSecurityConfig.User("test_union_of_non_overlapping_roles_user").roles( + ROLE_NON_SENSITIVE_ONLY, + ROLE_MATCH_HISTORY_GENRE_ONLY + ); + @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .anonymousAuth(false) @@ -171,7 +234,12 @@ public class DlsIntegrationTests { READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING, READ_WHERE_STARS_LESS_THAN_THREE, READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE, - READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST, + USER_NON_SENSITIVE_ONLY, + USER_ALLOW_ALL, + USER_MATCH_HISTORY_GENRE_ONLY, + USER_UNION_OF_OVERLAPPING_ROLES_NON_SENSITIVE_ONLY_AND_ALLOW_ALL, + USER_UNION_OF_NONOVERLAPPING_ROLES_NON_SENSITIVE_ONLY_AND_HISTORY_GENRE_ONLY ) .build(); @@ -217,6 +285,21 @@ public class DlsIntegrationTests { } }; + static final TreeMap> UNION_ROLE_TEST_DATA = new TreeMap<>() { + { + put("1", Map.of("genre", "History", "date", "01-01-2020", "sensitive", true)); + put("2", Map.of("genre", "History", "date", "01-01-2020", "sensitive", true)); + put("3", Map.of("genre", "History", "date", "01-01-2020", "sensitive", true)); + put("4", Map.of("genre", "History", "date", "01-01-2020", "sensitive", true)); + put("5", Map.of("genre", "History", "date", "01-01-2020", "sensitive", true)); + put("6", Map.of("genre", "Math", "date", "01-01-2020", "sensitive", false)); + put("7", Map.of("genre", "Math", "date", "01-01-2020", "sensitive", false)); + put("8", Map.of("genre", "Math", "date", "01-01-2020", "sensitive", false)); + put("9", Map.of("genre", "Math", "date", "01-01-2020", "sensitive", false)); + put("10", Map.of("genre", "Science", "date", "01-01-2020", "sensitive", true)); + } + }; + @BeforeClass public static void createTestData() { try (Client client = cluster.getInternalNodeClient()) { @@ -274,6 +357,10 @@ public static void createTestData() { ) ) .actionGet(); + + UNION_ROLE_TEST_DATA.forEach((index, document) -> { + client.prepareIndex(UNION_TEST_INDEX_NAME).setId(index).setRefreshPolicy(IMMEDIATE).setSource(document).get(); + }); } } @@ -281,7 +368,7 @@ public static void createTestData() { public void testShouldSearchAll() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchRequest searchRequest = searchRequestWithSort(FIRST_INDEX_NAME); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -293,7 +380,7 @@ public void testShouldSearchAll() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchRequest = searchRequestWithSort(SECOND_INDEX_NAME); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -301,7 +388,7 @@ public void testShouldSearchAll() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); } try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_FIRST_AND_SECOND_USER)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchRequest searchRequest = searchRequestWithSort(FIRST_INDEX_NAME); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -313,7 +400,7 @@ public void testShouldSearchAll() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchRequest = searchRequestWithSort(SECOND_INDEX_NAME); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -326,14 +413,14 @@ public void testShouldSearchAll() throws IOException { public void testShouldSearchI1_S2I2_S3() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchRequest searchRequest = searchRequestWithSort(FIRST_INDEX_NAME); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_STRING)); - searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchRequest = searchRequestWithSort(SECOND_INDEX_NAME); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -349,7 +436,7 @@ public void testShouldSearchI1_S3I1_S6I2_S2() throws IOException { READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE ) ) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchRequest searchRequest = searchRequestWithSort(FIRST_INDEX_NAME); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -357,7 +444,7 @@ public void testShouldSearchI1_S3I1_S6I2_S2() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_UNKNOWN)); - searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchRequest = searchRequestWithSort(SECOND_INDEX_NAME); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -373,7 +460,7 @@ public void testShouldSearchI1_S1I1_S3I2_S2I2_S4() throws IOException { READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST ) ) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchRequest searchRequest = searchRequestWithSort(FIRST_INDEX_NAME); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -381,7 +468,7 @@ public void testShouldSearchI1_S1I1_S3I2_S2I2_S4() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_FIRST)); - searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchRequest = searchRequestWithSort(SECOND_INDEX_NAME); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -394,7 +481,7 @@ public void testShouldSearchI1_S1I1_S3I2_S2I2_S4() throws IOException { public void testShouldSearchStarsLessThanThree() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_STARS_LESS_THAN_THREE)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchRequest searchRequest = searchRequestWithSort(FIRST_INDEX_NAME); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -402,7 +489,7 @@ public void testShouldSearchStarsLessThanThree() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); - searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchRequest = searchRequestWithSort(SECOND_INDEX_NAME); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -417,7 +504,7 @@ public void testSearchForAllDocumentsWithIndexPattern() throws IOException { // DLS try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { - SearchRequest searchRequest = new SearchRequest("*".concat(FIRST_INDEX_NAME)); + SearchRequest searchRequest = searchRequestWithSort("*".concat(FIRST_INDEX_NAME)); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -429,7 +516,7 @@ public void testSearchForAllDocumentsWithIndexPattern() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - searchRequest = new SearchRequest("*".concat(SECOND_INDEX_NAME)); + searchRequest = searchRequestWithSort("*".concat(SECOND_INDEX_NAME)); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -442,7 +529,7 @@ public void testSearchForAllDocumentsWithIndexPattern() throws IOException { public void testSearchForAllDocumentsWithAlias() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_ALIAS); + SearchRequest searchRequest = searchRequestWithSort(FIRST_INDEX_ALIAS); SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); @@ -454,12 +541,15 @@ public void testSearchForAllDocumentsWithAlias() throws IOException { assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - searchRequest = new SearchRequest("*".concat(SECOND_INDEX_NAME)); + searchRequest = searchRequestWithSort("*".concat(SECOND_INDEX_NAME)); searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); assertThat(searchResponse, isSuccessfulSearchResponse()); assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_STRING)); + assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_FIRST)); } } @@ -513,4 +603,121 @@ public void testAggregateAndComputeStarRatings() throws IOException { assertThat(((ParsedAvg) actualAggregation).getValue(), is(1.5)); } } + + @Test + public void testOverlappingRoleUnionSearchFiltering() throws Exception { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_NON_SENSITIVE_ONLY)) { + SearchRequest searchRequest = new SearchRequest(UNION_TEST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertSearchResponseHitsEqualTo(searchResponse, 4); + + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + UNION_ROLE_TEST_DATA.entrySet() + .stream() + .filter(e -> e.getValue().get("sensitive").equals(false)) + .map(e -> Pair.of(UNION_TEST_INDEX_NAME, e.getKey())) + .collect(Collectors.toList()) + ) + ); + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOW_ALL)) { + SearchRequest searchRequest = new SearchRequest(UNION_TEST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertSearchResponseHitsEqualTo(searchResponse, 10); + } + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_UNION_OF_OVERLAPPING_ROLES_NON_SENSITIVE_ONLY_AND_ALLOW_ALL + ) + ) { + SearchRequest searchRequest = new SearchRequest(UNION_TEST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertSearchResponseHitsEqualTo(searchResponse, 10); + + // shows that roles are additive and the overlapping role with less filtering is used + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + UNION_ROLE_TEST_DATA.keySet().stream().map(id -> Pair.of(UNION_TEST_INDEX_NAME, id)).collect(Collectors.toList()) + ) + ); + } + } + + @Test + @SuppressWarnings("unchecked") + public void testNonOverlappingRoleUnionSearchFiltering() throws Exception { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_NON_SENSITIVE_ONLY)) { + SearchRequest searchRequest = new SearchRequest(UNION_TEST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertSearchResponseHitsEqualTo(searchResponse, 4); + + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + UNION_ROLE_TEST_DATA.entrySet() + .stream() + .filter(e -> e.getValue().get("sensitive").equals(false)) + .map(e -> Pair.of(UNION_TEST_INDEX_NAME, e.getKey())) + .collect(Collectors.toList()) + ) + ); + } + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_MATCH_HISTORY_GENRE_ONLY)) { + SearchRequest searchRequest = new SearchRequest(UNION_TEST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertSearchResponseHitsEqualTo(searchResponse, 5); + + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + UNION_ROLE_TEST_DATA.entrySet() + .stream() + .filter(e -> e.getValue().get("genre").equals("History")) + .map(e -> Pair.of(UNION_TEST_INDEX_NAME, e.getKey())) + .collect(Collectors.toList()) + ) + ); + } + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_UNION_OF_NONOVERLAPPING_ROLES_NON_SENSITIVE_ONLY_AND_HISTORY_GENRE_ONLY + ) + ) { + SearchRequest searchRequest = new SearchRequest(UNION_TEST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertSearchResponseHitsEqualTo(searchResponse, 9); + + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + UNION_ROLE_TEST_DATA.keySet() + .stream() + .filter(id -> !id.equals("10")) + .map(id -> Pair.of(UNION_TEST_INDEX_NAME, id)) + .collect(Collectors.toList()) + ) + ); + + // shows that the roles are additive, but excludes one document since the DLS filters for both roles do not account for this + assertThat(searchResponse, not(searchHitsContainDocumentsInAnyOrder(Pair.of(UNION_TEST_INDEX_NAME, "10")))); + } + } + + private void assertSearchResponseHitsEqualTo(SearchResponse searchResponse, int hits) throws Exception { + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(hits)); + } } diff --git a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java index 207564daaa..3a50a4b1f6 100644 --- a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java +++ b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java @@ -39,10 +39,14 @@ import org.opensearch.client.Response; import org.opensearch.client.RestHighLevelClient; import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.TestSecurityConfig.User; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import static org.apache.http.HttpStatus.SC_CREATED; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.aMapWithSize; import static org.hamcrest.Matchers.allOf; @@ -51,6 +55,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasKey; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.nullValue; import static org.opensearch.action.admin.indices.alias.IndicesAliasesRequest.AliasActions.Type.ADD; import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; @@ -127,13 +132,17 @@ public class DoNotFailOnForbiddenTests { .on(MARVELOUS_SONGS) ); + private static final User STATS_USER = new User("stats_user").roles( + new Role("test_role").clusterPermissions("cluster:monitor/*").indexPermissions("read", "indices:monitor/*").on("hi1") + ); + private static final String BOTH_INDEX_ALIAS = "both-indices"; private static final String FORBIDDEN_INDEX_ALIAS = "forbidden-index"; @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER, LIMITED_USER) + .users(ADMIN_USER, LIMITED_USER, STATS_USER) .anonymousAuth(false) .doNotFailOnForbidden(true) .build(); @@ -434,4 +443,39 @@ public void shouldPerformCatIndices_positive() throws IOException { } } + @Test + public void checkStatsApi() { + // As admin creates 2 documents in different indices, can find both indices in search, cat indice & stats APIs + try (final TestRestClient client = cluster.getRestClient(ADMIN_USER.getName(), ADMIN_USER.getPassword())) { + final HttpResponse createDoc1 = client.postJson("hi1/_doc?refresh=true", "{\"hi\":\"Hello1\"}"); + createDoc1.assertStatusCode(SC_CREATED); + final HttpResponse createDoc2 = client.postJson("hi2/_doc?refresh=true", "{\"hi\":\"Hello2\"}"); + createDoc2.assertStatusCode(SC_CREATED); + + final HttpResponse search = client.postJson("hi*/_search", "{}"); + assertThat("Unexpected document results in search:" + search.getBody(), search.getBody(), containsString("2")); + + final HttpResponse catIndices = client.get("_cat/indices"); + assertThat("Expected cat indices: " + catIndices.getBody(), catIndices.getBody(), containsString("hi1")); + assertThat("Expected cat indices: " + catIndices.getBody(), catIndices.getBody(), containsString("hi2")); + + final HttpResponse stats = client.get("hi*/_stats?filter_path=indices.*.uuid"); + assertThat("Expected stats indices: " + stats.getBody(), stats.getBody(), containsString("hi1")); + assertThat("Expected stats indices: " + stats.getBody(), stats.getBody(), containsString("hi2")); + } + + // As user who can only see the index "hi1" make sure that DNFOF is filtering out "hi2" + try (final TestRestClient client = cluster.getRestClient(STATS_USER.getName(), STATS_USER.getPassword())) { + final HttpResponse search = client.postJson("hi*/_search", "{}"); + assertThat("Unexpected document results in search:" + search.getBody(), search.getBody(), containsString("1")); + + final HttpResponse catIndices = client.get("_cat/indices"); + assertThat("Expected cat indices: " + catIndices.getBody(), catIndices.getBody(), containsString("hi1")); + assertThat("Unexpected cat indices: " + catIndices.getBody(), catIndices.getBody(), not(containsString("hi2"))); + + final HttpResponse stats = client.get("hi*/_stats?filter_path=indices.*.uuid"); + assertThat("Expected stats indices: " + stats.getBody(), stats.getBody(), containsString("hi1")); + assertThat("Unexpected stats indices: " + stats.getBody(), stats.getBody(), not(containsString("hi2"))); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java b/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java index 3d634c4a5d..8d900639c2 100644 --- a/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java +++ b/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java @@ -14,13 +14,12 @@ import com.carrotsearch.randomizedtesting.RandomizedRunner; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.commons.lang3.tuple.Pair; +import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.OpenSearchStatusException; import org.opensearch.action.admin.indices.alias.IndicesAliasesRequest; import org.opensearch.action.index.IndexRequest; import org.opensearch.action.search.CreatePitRequest; @@ -33,7 +32,6 @@ import org.opensearch.client.Client; import org.opensearch.client.RestHighLevelClient; import org.opensearch.common.unit.TimeValue; -import org.opensearch.core.rest.RestStatus; import org.opensearch.search.builder.PointInTimeBuilder; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.test.framework.TestSecurityConfig; @@ -133,6 +131,13 @@ public static void createTestData() { } } + @Before + public void cleanUpPits() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + restHighLevelClient.deleteAllPits(DEFAULT); + } + } + @ClassRule public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .anonymousAuth(false) @@ -180,11 +185,9 @@ public void createPitWithIndexAlias_negative() throws IOException { } } - @Ignore("Pretty sure cleanUpPits is returning before all of the PITs have actually been deleted") @Test public void listAllPits_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { - cleanUpPits(); String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); @@ -247,11 +250,9 @@ public void deletePitCreatedWithIndexAlias_negative() throws IOException { } } - @Ignore("Pretty sure cleanUpPits is returning before all of the PITs have actually been deleted") @Test public void deleteAllPits_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { - cleanUpPits(); String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); @@ -381,7 +382,7 @@ public void listPitSegmentsCreatedWithIndexAlias_negative() throws IOException { @Test public void listAllPitSegments_positive() { try (TestRestClient restClient = cluster.getRestClient(POINT_IN_TIME_USER)) { - HttpResponse response = restClient.get("/_cat/pit_segments/_all"); + HttpResponse response = restClient.get("_cat/pit_segments/_all"); response.assertStatusCode(OK.getStatus()); } @@ -390,7 +391,7 @@ public void listAllPitSegments_positive() { @Test public void listAllPitSegments_negative() { try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { - HttpResponse response = restClient.get("/_cat/pit_segments/_all"); + HttpResponse response = restClient.get("_cat/pit_segments/_all"); response.assertStatusCode(FORBIDDEN.getStatus()); } @@ -410,20 +411,4 @@ private String createPitForIndices(String... indices) throws IOException { } } - /** - * Deletes all PITs. - */ - public void cleanUpPits() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - try { - restHighLevelClient.deleteAllPits(DEFAULT); - } catch (OpenSearchStatusException ex) { - if (ex.status() != RestStatus.NOT_FOUND) { - throw ex; - } - // tried to remove pits but no pit exists - } - } - } - } diff --git a/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java b/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java index 61a1e32023..7264a33542 100644 --- a/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java +++ b/src/integrationTest/java/org/opensearch/security/ResourceFocusedTests.java @@ -41,6 +41,8 @@ import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; @@ -127,11 +129,13 @@ private void runResourceTest( final var requests = AsyncActions.generate(() -> { final HttpPost post = new HttpPost(client.getHttpServerUri() + requestPath); post.setEntity(new ByteArrayEntity(compressedRequestBody, ContentType.APPLICATION_JSON)); - return client.executeRequest(post); + TestRestClient.HttpResponse response = client.executeRequest(post); + return response.getStatusCode(); }, parrallelism, totalNumberOfRequests); - AsyncActions.getAll(requests, 2, TimeUnit.MINUTES) - .forEach((response) -> { response.assertStatusCode(HttpStatus.SC_UNAUTHORIZED); }); + AsyncActions.getAll(requests, 2, TimeUnit.MINUTES).forEach((responseCode) -> { + assertThat(responseCode, equalTo(HttpStatus.SC_UNAUTHORIZED)); + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java index e39eeeca61..cbb5ec11f0 100644 --- a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java +++ b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java @@ -13,6 +13,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import com.google.common.base.Stopwatch; @@ -23,11 +25,12 @@ import org.junit.Before; import org.junit.BeforeClass; import org.junit.ClassRule; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; +import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; import org.opensearch.action.admin.cluster.repositories.delete.DeleteRepositoryRequest; import org.opensearch.action.admin.cluster.repositories.put.PutRepositoryRequest; import org.opensearch.action.admin.cluster.snapshots.create.CreateSnapshotResponse; @@ -83,6 +86,7 @@ import org.opensearch.client.indices.PutMappingRequest; import org.opensearch.client.indices.ResizeRequest; import org.opensearch.client.indices.ResizeResponse; +import org.opensearch.cluster.health.ClusterHealthStatus; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.IndexTemplateMetadata; import org.opensearch.common.settings.Settings; @@ -94,6 +98,7 @@ import org.opensearch.index.reindex.ReindexRequest; import org.opensearch.repositories.RepositoryMissingException; import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.auditlog.AuditLog; import org.opensearch.test.framework.AuditCompliance; import org.opensearch.test.framework.AuditConfiguration; import org.opensearch.test.framework.AuditFilters; @@ -119,6 +124,7 @@ import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.opensearch.client.RequestOptions.DEFAULT; import static org.opensearch.core.rest.RestStatus.ACCEPTED; +import static org.opensearch.core.rest.RestStatus.BAD_REQUEST; import static org.opensearch.core.rest.RestStatus.FORBIDDEN; import static org.opensearch.core.rest.RestStatus.INTERNAL_SERVER_ERROR; import static org.opensearch.rest.RestRequest.Method.DELETE; @@ -335,25 +341,27 @@ public class SearchOperationTest { * indices with names prefixed by the {@link #INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX} */ static final User USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES = new User("index-operation-tester").roles( - new Role("index-manager").indexPermissions( - "indices:admin/create", - "indices:admin/get", - "indices:admin/delete", - "indices:admin/close", - "indices:admin/close*", - "indices:admin/open", - "indices:admin/resize", - "indices:monitor/stats", - "indices:monitor/settings/get", - "indices:admin/settings/update", - "indices:admin/mapping/put", - "indices:admin/mappings/get", - "indices:admin/cache/clear", - "indices:admin/aliases" - ).on(INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*")) + new Role("index-manager").clusterPermissions("cluster:monitor/health") + .indexPermissions( + "indices:admin/create", + "indices:admin/get", + "indices:admin/delete", + "indices:admin/close", + "indices:admin/close*", + "indices:admin/open", + "indices:admin/resize", + "indices:monitor/stats", + "indices:monitor/settings/get", + "indices:admin/settings/update", + "indices:admin/mapping/put", + "indices:admin/mappings/get", + "indices:admin/cache/clear", + "indices:admin/aliases" + ) + .on(INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*")) ); - private static User USER_ALLOWED_TO_CREATE_INDEX = new User("user-allowed-to-create-index").roles( + private static final User USER_ALLOWED_TO_CREATE_INDEX = new User("user-allowed-to-create-index").roles( new Role("create-index-role").indexPermissions("indices:admin/create").on("*") ); @@ -456,7 +464,7 @@ public void cleanData() throws ExecutionException, InterruptedException { if (indicesExistsResponse.isExists()) { DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexToBeDeleted); indices.delete(deleteIndexRequest).actionGet(); - Awaitility.await().ignoreExceptions().until(() -> indices.exists(indicesExistsRequest).get().isExists() == false); + Awaitility.await().ignoreExceptions().until(() -> !indices.exists(indicesExistsRequest).get().isExists()); } } @@ -970,12 +978,11 @@ public void shouldIndexDocumentInBulkRequest_positive() throws IOException { } auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertAtLeast(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER));// sometimes 4 or 6 - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest"));// sometimes 2 or 4 + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } - @Ignore("Audit log verification is shown to be flaky in this test") @Test public void shouldIndexDocumentInBulkRequest_partiallyPositive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { @@ -998,10 +1005,10 @@ public void shouldIndexDocumentInBulkRequest_partiallyPositive() throws IOExcept } auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test @@ -1030,7 +1037,6 @@ public void shouldIndexDocumentInBulkRequest_negative() throws IOException { auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); } - @Ignore("Audit log verification is shown to be flaky in this test") @Test public void shouldUpdateDocumentsInBulk_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { @@ -1052,13 +1058,10 @@ public void shouldUpdateDocumentsInBulk_positive() throws IOException { } auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); } - @Ignore("Audit log verification is shown to be flaky in this test") @Test public void shouldUpdateDocumentsInBulk_partiallyPositive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { @@ -1081,10 +1084,10 @@ public void shouldUpdateDocumentsInBulk_partiallyPositive() throws IOException { } auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test @@ -1114,6 +1117,10 @@ public void shouldUpdateDocumentsInBulk_negative() throws IOException { @Test public void shouldDeleteDocumentInBulk_positive() throws IOException { + // create index + Settings sourceIndexSettings = Settings.builder().put("index.number_of_replicas", 2).put("index.number_of_shards", 2).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, sourceIndexSettings); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); @@ -1138,14 +1145,16 @@ public void shouldDeleteDocumentInBulk_positive() throws IOException { } auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactly(2, auditPredicate(null).withLayer(AuditLog.Origin.TRANSPORT)); + auditLogsRule.assertAtLeastTransportMessages(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertAtLeastTransportMessages(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); } - @Ignore("Audit log verification is shown to be flaky in this test") @Test public void shouldDeleteDocumentInBulk_partiallyPositive() throws IOException { + Settings indexSettings = Settings.builder().put("index.number_of_replicas", 0).put("index.number_of_shards", 1).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); @@ -1170,10 +1179,8 @@ public void shouldDeleteDocumentInBulk_partiallyPositive() throws IOException { } auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test @@ -1202,7 +1209,6 @@ public void shouldDeleteDocumentInBulk_negative() throws IOException { } - @Ignore("Seems like reindixing isn't completing before the test proceeds") @Test public void shouldReindexDocuments_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { @@ -1221,14 +1227,13 @@ public void shouldReindexDocuments_positive() throws IOException { auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(REINDEXING_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(REINDEXING_USER, "PutMappingRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "CreateIndexRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchScrollRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(REINDEXING_USER)); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(REINDEXING_USER)); auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(REINDEXING_USER, "PutMappingRequest")); } - @Ignore("Seems like reindixing isn't completing before the test proceeds") @Test public void shouldReindexDocuments_negativeSource() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { @@ -1243,7 +1248,6 @@ public void shouldReindexDocuments_negativeSource() throws IOException { auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "SearchRequest")); } - @Ignore("Seems like reindixing isn't completing before the test proceeds") @Test public void shouldReindexDocuments_negativeDestination() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { @@ -1262,7 +1266,6 @@ public void shouldReindexDocuments_negativeDestination() throws IOException { auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); } - @Ignore("Seems like reindixing isn't completing before the test proceeds") @Test public void shouldReindexDocuments_negativeSourceAndDestination() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { @@ -1335,7 +1338,6 @@ public void shouldDeleteDocument_negative() throws IOException { } } - @Ignore("Create alias / delete alias isn't resolving in a timely manner for this test") @Test public void shouldCreateAlias_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { @@ -1349,11 +1351,10 @@ public void shouldCreateAlias_positive() throws IOException { assertThat(internalClient, clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1)); } auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); - auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + auditLogsRule.assertExactlyOne(auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); } - @Ignore("Create alias / delete alias isn't resolving in a timely manner for this test") @Test public void shouldCreateAlias_negative() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { @@ -1371,7 +1372,6 @@ public void shouldCreateAlias_negative() throws IOException { auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); } - @Ignore("Create alias / delete alias isn't resolving in a timely manner for this test") @Test public void shouldDeleteAlias_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { @@ -1388,8 +1388,8 @@ public void shouldDeleteAlias_positive() throws IOException { assertThat(internalClient, not(clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1))); } auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); - auditLogsRule.assertExactly(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); } @Test @@ -1409,7 +1409,6 @@ public void shouldDeleteAlias_negative() throws IOException { auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); } - @Ignore("Create alias / delete alias isn't resolving in a timely manner for this test") @Test public void shouldCreateIndexTemplate_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { @@ -1433,12 +1432,12 @@ public void shouldCreateIndexTemplate_positive() throws IOException { } auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/0001")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(8, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test @@ -1471,9 +1470,9 @@ public void shouldDeleteTemplate_positive() throws IOException { } auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_template/musical-index-template")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteIndexTemplateRequest")); - auditLogsRule.assertExactly(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "DeleteIndexTemplateRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); } @Test @@ -1491,7 +1490,6 @@ public void shouldDeleteTemplate_negative() throws IOException { auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteIndexTemplateRequest")); } - @Ignore("Create alias / delete alias isn't resolving in a timely manner for this test") @Test public void shouldUpdateTemplate_positive() throws IOException { try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { @@ -1519,12 +1517,12 @@ public void shouldUpdateTemplate_positive() throws IOException { } auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/000one")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(10, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(3, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test @@ -1614,7 +1612,7 @@ public void shouldCreateSnapshotRepository_positive() throws IOException { assertThat(internalClient, clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME)); } auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); } @Test @@ -1650,8 +1648,8 @@ public void shouldDeleteSnapshotRepository_positive() throws IOException { auditLogsRule.assertExactlyOne( userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository") ); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "DeleteRepositoryRequest")); } @Test @@ -1668,9 +1666,10 @@ public void shouldDeleteSnapshotRepository_negative() throws IOException { auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteRepositoryRequest")); } - @Test // Bug which can be reproduced with the below test: https://github.com/opensearch-project/security/issues/2169 + @Test public void shouldCreateSnapshot_positive() throws IOException { final String snapshotName = "snapshot-positive-test"; + long snapshotGetCount; try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); @@ -1679,20 +1678,21 @@ public void shouldCreateSnapshot_positive() throws IOException { assertThat(response, notNullValue()); assertThat(response.status(), equalTo(RestStatus.ACCEPTED)); - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); assertThat(internalClient, clusterContainSuccessSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); } auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); auditLogsRule.assertExactlyOne( userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/snapshot-positive-test") ); - auditLogsRule.assertAtLeast( - 1, - userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/snapshot-positive-test") + auditLogsRule.assertExactly( + snapshotGetCount, + userAuthenticated(LIMITED_WRITE_USER).withEffectiveUser(LIMITED_WRITE_USER) + .withRestRequest(GET, "/_snapshot/test-snapshot-repository/snapshot-positive-test") ); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertAtLeast(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); } @Test @@ -1717,12 +1717,13 @@ public void shouldCreateSnapshot_negative() throws IOException { @Test public void shouldDeleteSnapshot_positive() throws IOException { String snapshotName = "delete-snapshot-positive"; + long snapshotGetCount; try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); restHighLevelClient.snapshot(); steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); var response = steps.deleteSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); @@ -1736,24 +1737,25 @@ public void shouldDeleteSnapshot_positive() throws IOException { auditLogsRule.assertExactlyOne( userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") ); - auditLogsRule.assertAtLeast( - 1, + auditLogsRule.assertExactly( + snapshotGetCount, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") ); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "DeleteSnapshotRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); } @Test public void shouldDeleteSnapshot_negative() throws IOException { String snapshotName = "delete-snapshot-negative"; + long snapshotGetCount; try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); } try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); @@ -1768,23 +1770,27 @@ public void shouldDeleteSnapshot_negative() throws IOException { auditLogsRule.assertExactlyOne( userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") ); - auditLogsRule.assertAtLeast( - 1, + auditLogsRule.assertExactly( + snapshotGetCount, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") ); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertExactly(1, missingPrivilege(LIMITED_READ_USER, "DeleteSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteSnapshotRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); } - @Ignore("Audit log entries verifcation isn't always consistant") @Test public void shouldRestoreSnapshot_positive() throws IOException { final String snapshotName = "restore-snapshot-positive"; + long snapshotGetCount; + AtomicInteger restoredCount = new AtomicInteger(); try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); // 1. create some documents + Settings indexSettings = Settings.builder().put("index.number_of_replicas", 0).put("index.number_of_shards", 1).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); + BulkRequest bulkRequest = new BulkRequest(); bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); @@ -1798,7 +1804,7 @@ public void shouldRestoreSnapshot_positive() throws IOException { steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); // 4. wait till snapshot is ready - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); // 5. introduce some changes bulkRequest = new BulkRequest(); @@ -1818,8 +1824,12 @@ public void shouldRestoreSnapshot_positive() throws IOException { CountRequest countRequest = new CountRequest(RESTORED_SONG_INDEX_NAME); Awaitility.await() .ignoreExceptions() + .pollInterval(100, TimeUnit.MILLISECONDS) .alias("Index contains proper number of documents restored from snapshot.") - .until(() -> restHighLevelClient.count(countRequest, DEFAULT).getCount() == 2); + .until(() -> { + restoredCount.incrementAndGet(); + return restHighLevelClient.count(countRequest, DEFAULT).getCount() == 2; + }); // 8. verify that document are present in restored index assertThat( @@ -1843,28 +1853,33 @@ public void shouldRestoreSnapshot_positive() throws IOException { "/_snapshot/test-snapshot-repository/restore-snapshot-positive/_restore" ) ); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/restored_write_song_index/_count")); + auditLogsRule.assertExactly( + restoredCount.get(), + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/restored_write_song_index/_count") + ); auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertAtLeast( - 1, + auditLogsRule.assertExactly( + snapshotGetCount, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/restore-snapshot-positive") ); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "SearchRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertExactly(restoredCount.get(), grantedPrivilege(LIMITED_WRITE_USER, "SearchRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test public void shouldRestoreSnapshot_failureForbiddenIndex() throws IOException { final String snapshotName = "restore-snapshot-negative-forbidden-index"; String restoreToIndex = "forbidden_index"; + long snapshotGetCount; + Settings indexSettings = Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); // 1. create some documents BulkRequest bulkRequest = new BulkRequest(); @@ -1880,7 +1895,7 @@ public void shouldRestoreSnapshot_failureForbiddenIndex() throws IOException { steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); // 4. wait till snapshot is ready - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); // 5. restore the snapshot assertThatThrownBy( @@ -1906,27 +1921,28 @@ public void shouldRestoreSnapshot_failureForbiddenIndex() throws IOException { ) ); auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertAtLeast( - 1, + auditLogsRule.assertExactly( + snapshotGetCount, userAuthenticated(LIMITED_WRITE_USER).withRestRequest( GET, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index" ) ); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertAtLeastTransportMessages(1, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyScanAll(1, missingPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test public void shouldRestoreSnapshot_failureOperationForbidden() throws IOException { String snapshotName = "restore-snapshot-negative-forbidden-operation"; + long snapshotGetCount; + Settings indexSettings = Settings.builder().put("index.number_of_shards", 1).put("index.number_of_replicas", 0).build(); + IndexOperationsHelper.createIndex(cluster, WRITE_SONG_INDEX_NAME, indexSettings); try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); // 1. create some documents @@ -1943,7 +1959,7 @@ public void shouldRestoreSnapshot_failureOperationForbidden() throws IOException steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); // 4. wait till snapshot is ready - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + snapshotGetCount = steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); } // 5. restore the snapshot try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { @@ -1971,21 +1987,19 @@ public void shouldRestoreSnapshot_failureOperationForbidden() throws IOException ) ); auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertAtLeast( - 1, + auditLogsRule.assertExactly( + snapshotGetCount, userAuthenticated(LIMITED_WRITE_USER).withRestRequest( GET, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation" ) ); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(1, missingPrivilege(LIMITED_READ_USER, "RestoreSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyScanAll(1, missingPrivilege(LIMITED_READ_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertExactly(snapshotGetCount, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertAtLeastTransportMessages(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); } @Test @@ -2123,11 +2137,11 @@ public void shouldDeleteIndexByAliasRequest_positive() throws IOException { userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest(POST, "/_aliases") ); auditLogsRule.assertExactly( - 2, + 1, grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "IndicesAliasesRequest") ); auditLogsRule.assertExactly( - 2, + 1, auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES) ); } @@ -2272,14 +2286,15 @@ public void openIndex_negative() throws IOException { } @Test - @Ignore // required permissions: "indices:admin/resize", "indices:monitor/stats - // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. - // Issue: https://github.com/opensearch-project/security/issues/2141 public void shrinkIndex_positive() throws IOException { String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_source"); - Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).put("index.number_of_shards", 2).build(); String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_target"); + Settings sourceIndexSettings = Settings.builder() + .put("index.number_of_replicas", 1) + .put("index.blocks.write", true) + .put("index.number_of_shards", 4) + .build(); IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); try ( @@ -2287,6 +2302,17 @@ public void shrinkIndex_positive() throws IOException { USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES ) ) { + ClusterHealthResponse healthResponse = restHighLevelClient.cluster() + .health( + new ClusterHealthRequest(sourceIndexName).waitForNoRelocatingShards(true) + .waitForActiveShards(4) + .waitForNoInitializingShards(true) + .waitForGreenStatus(), + DEFAULT + ); + + assertThat(healthResponse.getStatus(), is(ClusterHealthStatus.GREEN)); + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); ResizeResponse response = restHighLevelClient.indices().shrink(resizeRequest, DEFAULT); @@ -2329,10 +2355,7 @@ public void shrinkIndex_negative() throws IOException { } @Test - @Ignore // required permissions: "indices:admin/resize", "indices:monitor/stats - // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. - // Issue: https://github.com/opensearch-project/security/issues/2141 public void cloneIndex_positive() throws IOException { String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_positive_source"); Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).build(); @@ -2349,6 +2372,10 @@ public void cloneIndex_positive() throws IOException { assertThat(response, isSuccessfulResizeResponse(targetIndexName)); assertThat(cluster, indexExists(targetIndexName)); + + // can't clone the same index twice, target already exists + ResizeRequest repeatResizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + assertThatThrownBy(() -> restHighLevelClient.indices().clone(repeatResizeRequest, DEFAULT), statusException(BAD_REQUEST)); } } @@ -2386,10 +2413,7 @@ public void cloneIndex_negative() throws IOException { } @Test - @Ignore // required permissions: "indices:admin/resize", "indices:monitor/stats - // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. - // Issue: https://github.com/opensearch-project/security/issues/2141 public void splitIndex_positive() throws IOException { String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_positive_source"); Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).build(); @@ -2704,11 +2728,11 @@ public void shouldCreateIndexWithAlias_positive() throws IOException { ) ); auditLogsRule.assertExactly( - 2, + 1, grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "CreateIndexRequest") ); auditLogsRule.assertExactly( - 2, + 1, auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES) ); } diff --git a/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java index 164b2cb714..81460d3d91 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java @@ -10,6 +10,7 @@ package org.opensearch.security; import java.io.File; +import java.nio.file.Path; import org.opensearch.security.tools.SecurityAdmin; import org.opensearch.test.framework.certificate.TestCertificates; @@ -44,4 +45,21 @@ public int updateRoleMappings(File roleMappingsConfigurationFile) throws Excepti return SecurityAdmin.execute(commandLineArguments); } + + public int runSecurityAdmin(Path configurationFolder) throws Exception { + String[] commandLineArguments = { + "-cacert", + certificates.getRootCertificate().getAbsolutePath(), + "-cert", + certificates.getAdminCertificate().getAbsolutePath(), + "-key", + certificates.getAdminKey(null).getAbsolutePath(), + "-nhnv", + "-p", + String.valueOf(port), + "-cd", + configurationFolder.toString() }; + + return SecurityAdmin.execute(commandLineArguments); + } } diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java new file mode 100644 index 0000000000..e6af5d58bb --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationBootstrapTests.java @@ -0,0 +1,160 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.io.FileUtils; +import org.awaitility.Awaitility; +import org.junit.AfterClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.ConfigHelper; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.ContextHeaderDecoratorClient; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.apache.http.HttpStatus.SC_SERVICE_UNAVAILABLE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SecurityConfigurationBootstrapTests { + + private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); + + private static LocalCluster createCluster(final Map nodeSettings) { + var cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .loadConfigurationIntoIndex(false) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .nodeSettings( + ImmutableMap.builder() + .put(SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName())) + .putAll(nodeSettings) + .build() + ) + .build(); + + cluster.before(); // normally invoked by JUnit rules when run as a class rule - this starts the cluster + return cluster; + } + + @AfterClass + public static void cleanConfigurationDirectory() throws IOException { + FileUtils.deleteDirectory(configurationFolder.toFile()); + } + + @Test + public void testInitializeWithSecurityAdminWhenNoBackgroundInitialization() throws Exception { + final var nodeSettings = ImmutableMap.builder() + .put(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false) + .put(SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, false) + .build(); + try (final LocalCluster cluster = createCluster(nodeSettings)) { + try (final TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + final var rolesMapsResponse = client.get("_plugins/_security/api/rolesmapping/readall"); + assertThat(rolesMapsResponse.getStatusCode(), equalTo(SC_SERVICE_UNAVAILABLE)); + assertThat(rolesMapsResponse.getBody(), containsString("OpenSearch Security not initialized")); + } + + final var securityAdminLauncher = new SecurityAdminLauncher(cluster.getHttpPort(), cluster.getTestCertificates()); + final int exitCode = securityAdminLauncher.runSecurityAdmin(configurationFolder); + assertThat(exitCode, equalTo(0)); + + try (final TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Awaitility.await() + .alias("Waiting for rolemapping 'readall' availability.") + .until(() -> client.get("_plugins/_security/api/rolesmapping/readall").getStatusCode(), equalTo(200)); + } + } + } + + @Test + public void shouldStillLoadSecurityConfigDuringBootstrapAndActiveConfigUpdateRequests() throws Exception { + final var nodeSettings = ImmutableMap.builder() + .put(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true) + .put(SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS, 5) + .build(); + try (final LocalCluster cluster = createCluster(nodeSettings)) { + try (final TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + cluster.getInternalNodeClient() + .admin() + .cluster() + .health(new ClusterHealthRequest(OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX).waitForGreenStatus()) + .actionGet(); + + // Make sure the cluster is unavalaible to authenticate with the security plugin even though it is green + final var authResponseWhenUnconfigured = client.getAuthInfo(); + authResponseWhenUnconfigured.assertStatusCode(503); + + final var internalNodeClient = new ContextHeaderDecoratorClient( + cluster.getInternalNodeClient(), + Map.of(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true") + ); + final var filesToUpload = ImmutableMap.builder() + .put("action_groups.yml", CType.ACTIONGROUPS) + .put("config.yml", CType.CONFIG) + .put("roles.yml", CType.ROLES) + .put("roles_mapping.yml", CType.ROLESMAPPING) + .put("tenants.yml", CType.TENANTS) + .build(); + + final String defaultInitDirectory = System.getProperty("security.default_init.dir") + "/"; + filesToUpload.forEach((fileName, ctype) -> { + try { + ConfigHelper.uploadFile( + internalNodeClient, + defaultInitDirectory + fileName, + OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX, + ctype, + DEFAULT_CONFIG_VERSION + ); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + }); + + Awaitility.await().alias("Load default configuration").pollInterval(Duration.ofMillis(100)).until(() -> { + // After the configuration has been loaded, the rest clients should be able to connect successfully + cluster.triggerConfigurationReloadForCTypes( + internalNodeClient, + List.of(CType.ACTIONGROUPS, CType.CONFIG, CType.ROLES, CType.ROLESMAPPING, CType.TENANTS), + true + ); + try (final TestRestClient freshClient = cluster.getRestClient(USER_ADMIN)) { + return client.getAuthInfo().getStatusCode(); + } + }, equalTo(200)); + } + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java index cc95f191f7..73c55bc667 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java @@ -13,8 +13,13 @@ import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; import org.awaitility.Awaitility; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -24,6 +29,8 @@ import org.junit.runner.RunWith; import org.opensearch.client.Client; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.framework.AsyncActions; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.TestSecurityConfig.User; import org.opensearch.test.framework.certificate.TestCertificates; @@ -33,6 +40,8 @@ import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.opensearch.action.support.WriteRequest.RefreshPolicy.IMMEDIATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST; @@ -70,7 +79,7 @@ public class SecurityConfigurationTests { SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, - true + false ) ) .build(); @@ -119,7 +128,7 @@ public void shouldCreateUserViaRestApi_failure() { @Test public void shouldAuthenticateAsAdminWithCertificate_positive() { try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + HttpResponse httpResponse = client.get("_plugins/_security/whoami"); httpResponse.assertStatusCode(200); assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("true")); @@ -130,7 +139,7 @@ public void shouldAuthenticateAsAdminWithCertificate_positive() { public void shouldAuthenticateAsAdminWithCertificate_negativeSelfSignedCertificate() { TestCertificates testCertificates = cluster.getTestCertificates(); try (TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=bond"))) { - HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + HttpResponse httpResponse = client.get("_plugins/_security/whoami"); httpResponse.assertStatusCode(200); assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); @@ -141,7 +150,7 @@ public void shouldAuthenticateAsAdminWithCertificate_negativeSelfSignedCertifica public void shouldAuthenticateAsAdminWithCertificate_negativeIncorrectDn() { TestCertificates testCertificates = cluster.getTestCertificates(); try (TestRestClient client = cluster.getRestClient(testCertificates.createAdminCertificate("CN=non_admin"))) { - HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + HttpResponse httpResponse = client.get("_plugins/_security/whoami"); httpResponse.assertStatusCode(200); assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); @@ -199,7 +208,7 @@ public void shouldStillWorkAfterUpdateOfSecurityConfig() { @Test public void shouldAccessIndexWithPlaceholder_positive() { try (TestRestClient client = cluster.getRestClient(LIMITED_USER)) { - HttpResponse httpResponse = client.get("/" + LIMITED_USER_INDEX + "/_doc/" + ID_1); + HttpResponse httpResponse = client.get(LIMITED_USER_INDEX + "/_doc/" + ID_1); httpResponse.assertStatusCode(200); } @@ -208,7 +217,7 @@ public void shouldAccessIndexWithPlaceholder_positive() { @Test public void shouldAccessIndexWithPlaceholder_negative() { try (TestRestClient client = cluster.getRestClient(LIMITED_USER)) { - HttpResponse httpResponse = client.get("/" + PROHIBITED_INDEX + "/_doc/" + ID_2); + HttpResponse httpResponse = client.get(PROHIBITED_INDEX + "/_doc/" + ID_2); httpResponse.assertStatusCode(403); } @@ -217,8 +226,8 @@ public void shouldAccessIndexWithPlaceholder_negative() { @Test public void shouldUseSecurityAdminTool() throws Exception { SecurityAdminLauncher securityAdminLauncher = new SecurityAdminLauncher(cluster.getHttpPort(), cluster.getTestCertificates()); - File rolesMapping = configurationDirectory.newFile("roles_mapping.yml"); - ConfigurationFiles.createRoleMappingFile(rolesMapping); + File rolesMapping = configurationDirectory.newFile(CType.ROLESMAPPING.configFileName()); + ConfigurationFiles.copyResourceToFile(CType.ROLESMAPPING.configFileName(), rolesMapping.toPath()); int exitCode = securityAdminLauncher.updateRoleMappings(rolesMapping); @@ -229,4 +238,39 @@ public void shouldUseSecurityAdminTool() throws Exception { .until(() -> client.get("_plugins/_security/api/rolesmapping/readall").getStatusCode(), equalTo(200)); } } + + @Test + public void testParallelTenantPutRequests() throws Exception { + final String TENANT_ENDPOINT = "_plugins/_security/api/tenants/tenant1"; + final String TENANT_BODY = "{\"description\":\"create new tenant\"}"; + final String TENANT_BODY_TWO = "{\"description\":\"update tenant\"}"; + + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + + final CountDownLatch countDownLatch = new CountDownLatch(1); + final List> conflictingRequests = AsyncActions.generate(() -> { + countDownLatch.await(); + return client.putJson(TENANT_ENDPOINT, TENANT_BODY); + }, 4, 4); + + // Make sure all requests start at the same time + countDownLatch.countDown(); + + AtomicInteger numCreatedResponses = new AtomicInteger(); + AsyncActions.getAll(conflictingRequests, 1, TimeUnit.SECONDS).forEach((response) -> { + assertThat(response.getStatusCode(), anyOf(equalTo(HttpStatus.SC_CREATED), equalTo(HttpStatus.SC_CONFLICT))); + if (response.getStatusCode() == HttpStatus.SC_CREATED) numCreatedResponses.getAndIncrement(); + }); + assertThat(numCreatedResponses.get(), equalTo(1)); // should only be one 201 + + TestRestClient.HttpResponse getResponse = client.get(TENANT_ENDPOINT); // make sure the one 201 works + assertThat(getResponse.getBody(), containsString("create new tenant")); + + TestRestClient.HttpResponse updateResponse = client.putJson(TENANT_ENDPOINT, TENANT_BODY_TWO); + assertThat(updateResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + getResponse = client.get(TENANT_ENDPOINT); // make sure update works + assertThat(getResponse.getBody(), containsString("update tenant")); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java b/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java index 28aa6abd43..a03891ecca 100644 --- a/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java +++ b/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java @@ -11,6 +11,8 @@ import java.io.IOException; import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; import org.awaitility.Awaitility; @@ -58,13 +60,21 @@ public CreateSnapshotResponse createSnapshot(String repositoryName, String snaps return snapshotClient.create(createSnapshotRequest, DEFAULT); } - public void waitForSnapshotCreation(String repositoryName, String snapshotName) { + public int waitForSnapshotCreation(String repositoryName, String snapshotName) { + AtomicInteger count = new AtomicInteger(); GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest(repositoryName, new String[] { snapshotName }); - Awaitility.await().alias("wait for snapshot creation").ignoreExceptions().until(() -> { - GetSnapshotsResponse snapshotsResponse = snapshotClient.get(getSnapshotsRequest, DEFAULT); - SnapshotInfo snapshotInfo = snapshotsResponse.getSnapshots().get(0); - return SnapshotState.SUCCESS.equals(snapshotInfo.state()); - }); + Awaitility.await() + .pollDelay(250, TimeUnit.MILLISECONDS) + .pollInterval(2, TimeUnit.SECONDS) + .alias("wait for snapshot creation") + .ignoreExceptions() + .until(() -> { + count.incrementAndGet(); + GetSnapshotsResponse snapshotsResponse = snapshotClient.get(getSnapshotsRequest, DEFAULT); + SnapshotInfo snapshotInfo = snapshotsResponse.getSnapshots().get(0); + return SnapshotState.SUCCESS.equals(snapshotInfo.state()); + }); + return count.get(); } // CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here diff --git a/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java b/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java index 25feffb2b4..2ea5b4c0b2 100644 --- a/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java +++ b/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java @@ -59,7 +59,7 @@ public void shouldGetIndicesWithoutAuthentication() { try (TestRestClient client = cluster.getRestClient()) { // request does not contains credential - HttpResponse response = client.get("/_cat/indices"); + HttpResponse response = client.get("_cat/indices"); // successful response is returned because the security plugin in SSL only mode // does not perform authentication and authorization diff --git a/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java new file mode 100644 index 0000000000..678b1df161 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/AbstractApiIntegrationTest.java @@ -0,0 +1,353 @@ +/* + * 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.api; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.List; +import java.util.StringJoiner; + +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import com.google.common.collect.ImmutableMap; +import org.apache.commons.io.FileUtils; +import org.apache.http.HttpStatus; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.awaitility.Awaitility; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.runner.RunWith; + +import org.opensearch.common.CheckedConsumer; +import org.opensearch.common.CheckedSupplier; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.security.ConfigurationFiles; +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.certificate.CertificateData; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.notNullValue; +import static org.opensearch.security.CrossClusterSearchTests.PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED; +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; +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; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; +import static org.opensearch.test.framework.TestSecurityConfig.REST_ADMIN_REST_API_ACCESS; + +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +public abstract class AbstractApiIntegrationTest extends RandomizedTest { + + private static final Logger LOGGER = LogManager.getLogger(TestSecurityConfig.class); + + public static final String NEW_USER = "new-user"; + + public static final String REST_ADMIN_USER = "rest-api-admin"; + + public static final String ADMIN_USER_NAME = "admin"; + + public static final String DEFAULT_PASSWORD = "secret"; + + public static final ToXContentObject EMPTY_BODY = (builder, params) -> builder.startObject().endObject(); + + public static Path configurationFolder; + + public static ImmutableMap.Builder clusterSettings = ImmutableMap.builder(); + + protected static TestSecurityConfig testSecurityConfig = new TestSecurityConfig(); + + public static LocalCluster localCluster; + + @BeforeClass + public static void startCluster() throws IOException { + configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + extendConfiguration(); + clusterSettings.put(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true) + .put(PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_admin__all_access", REST_ADMIN_REST_API_ACCESS)) + .put(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, randomBoolean()); + final var clusterManager = randomFrom(List.of(ClusterManager.THREE_CLUSTER_MANAGERS, ClusterManager.SINGLENODE)); + final var localClusterBuilder = new LocalCluster.Builder().clusterManager(clusterManager) + .nodeSettings(clusterSettings.buildKeepingLast()) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false); + localCluster = localClusterBuilder.build(); + localCluster.before(); + try (TestRestClient client = localCluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + Awaitility.await() + .alias("Load default configuration") + .until(() -> client.securityHealth().getTextFromJsonBody("/status"), equalTo("UP")); + } + } + + private static void extendConfiguration() throws IOException { + extendActionGroups(configurationFolder, testSecurityConfig.actionGroups()); + extendRoles(configurationFolder, testSecurityConfig.roles()); + extendRolesMapping(configurationFolder, testSecurityConfig.rolesMapping()); + extendUsers(configurationFolder, testSecurityConfig.getUsers()); + } + + private static void extendUsers(final Path configFolder, final List users) throws IOException { + if (users == null) return; + if (users.isEmpty()) return; + LOGGER.info("Adding users to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var u : users) { + LOGGER.info("\t\t - {}", u.getName()); + contentBuilder.field(u.getName()); + u.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.INTERNALUSERS, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static void extendActionGroups(final Path configFolder, final List actionGroups) + throws IOException { + if (actionGroups == null) return; + if (actionGroups.isEmpty()) return; + LOGGER.info("Adding action groups to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var ag : actionGroups) { + LOGGER.info("\t\t - {}", ag.name()); + contentBuilder.field(ag.name()); + ag.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.ACTIONGROUPS, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static void extendRoles(final Path configFolder, final List roles) throws IOException { + if (roles == null) return; + if (roles.isEmpty()) return; + LOGGER.info("Adding roles to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var r : roles) { + LOGGER.info("\t\t - {}", r.getName()); + contentBuilder.field(r.getName()); + r.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.ROLES, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static void extendRolesMapping(final Path configFolder, final List rolesMapping) + throws IOException { + if (rolesMapping == null) return; + if (rolesMapping.isEmpty()) return; + LOGGER.info("Adding roles mapping to the default configuration: "); + try (final var contentBuilder = XContentFactory.yamlBuilder()) { + contentBuilder.startObject(); + for (final var rm : rolesMapping) { + LOGGER.info("\t\t - {}", rm.name()); + contentBuilder.field(rm.name()); + rm.toXContent(contentBuilder, ToXContent.EMPTY_PARAMS); + } + contentBuilder.endObject(); + ConfigurationFiles.writeToConfig(CType.ROLESMAPPING, configFolder, removeDashes(contentBuilder.toString())); + } + } + + private static String removeDashes(final String content) { + return content.replace("---", ""); + } + + protected static String[] allRestAdminPermissions() { + final var permissions = new String[ENDPOINTS_WITH_PERMISSIONS.size() + 3]; // 2 actions for SSL + update config action + var counter = 0; + for (final var e : ENDPOINTS_WITH_PERMISSIONS.entrySet()) { + if (e.getKey() == Endpoint.SSL) { + permissions[counter] = e.getValue().build(CERTS_INFO_ACTION); + permissions[++counter] = e.getValue().build(RELOAD_CERTS_ACTION); + } else if (e.getKey() == Endpoint.CONFIG) { + permissions[counter++] = e.getValue().build(SECURITY_CONFIG_UPDATE); + } else { + permissions[counter++] = e.getValue().build(); + } + } + return permissions; + } + + protected static String restAdminPermission(Endpoint endpoint) { + return restAdminPermission(endpoint, null); + } + + protected static String restAdminPermission(Endpoint endpoint, String action) { + if (action != null) { + return ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(action); + } else { + return ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(); + } + } + + @AfterClass + public static void stopCluster() throws IOException { + if (localCluster != null) localCluster.close(); + FileUtils.deleteDirectory(configurationFolder.toFile()); + } + + protected void withUser(final String user, final CheckedConsumer restClientHandler) throws Exception { + withUser(user, DEFAULT_PASSWORD, restClientHandler); + } + + protected void withUser(final String user, final String password, final CheckedConsumer restClientHandler) + throws Exception { + try (TestRestClient client = localCluster.getRestClient(user, password)) { + restClientHandler.accept(client); + } + } + + protected void withUser( + final String user, + final CertificateData certificateData, + final CheckedConsumer restClientHandler + ) throws Exception { + withUser(user, DEFAULT_PASSWORD, certificateData, restClientHandler); + } + + protected void withUser( + final String user, + final String password, + final CertificateData certificateData, + final CheckedConsumer restClientHandler + ) throws Exception { + try (TestRestClient client = localCluster.getRestClient(user, password, certificateData)) { + restClientHandler.accept(client); + } + } + + protected String securityPath(String... path) { + final var fullPath = new StringJoiner("/"); + fullPath.add(randomFrom(List.of(LEGACY_OPENDISTRO_PREFIX, PLUGINS_PREFIX))); + if (path != null) { + for (final var p : path) + fullPath.add(p); + } + return fullPath.toString(); + } + + protected String api() { + return String.format("%s/api", securityPath()); + } + + protected String apiPath(final String... path) { + + final var fullPath = new StringJoiner("/"); + fullPath.add(api()); + + for (final var p : path) { + fullPath.add(p); + } + return fullPath.toString(); + } + + TestRestClient.HttpResponse badRequest(final CheckedSupplier endpointCallback) + throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + assertResponseBody(response.getBody()); + return response; + } + + TestRestClient.HttpResponse created(final CheckedSupplier endpointCallback) throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_CREATED)); + assertResponseBody(response.getBody()); + return response; + } + + TestRestClient.HttpResponse forbidden(final CheckedSupplier endpointCallback) throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertResponseBody(response.getBody()); + return response; + } + + TestRestClient.HttpResponse methodNotAllowed(final CheckedSupplier endpointCallback) + throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_METHOD_NOT_ALLOWED)); + assertResponseBody(response.getBody()); + return response; + } + + TestRestClient.HttpResponse notImplemented(final CheckedSupplier endpointCallback) + throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), is(HttpStatus.SC_NOT_IMPLEMENTED)); + assertResponseBody(response.getBody()); + return response; + } + + TestRestClient.HttpResponse notFound(final CheckedSupplier endpointCallback) throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_NOT_FOUND)); + assertResponseBody(response.getBody()); + return response; + } + + TestRestClient.HttpResponse ok(final CheckedSupplier endpointCallback) throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + assertResponseBody(response.getBody()); + return response; + } + + TestRestClient.HttpResponse unauthorized(final CheckedSupplier endpointCallback) + throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getStatusCode(), equalTo(HttpStatus.SC_UNAUTHORIZED)); + // TODO assert response body here + return response; + } + + void assertResponseBody(final String responseBody) { + assertThat(responseBody, notNullValue()); + assertThat(responseBody, not(equalTo(""))); + } + + void assertInvalidKeys(final TestRestClient.HttpResponse response, final String expectedInvalidKeys) { + assertThat(response.getBody(), response.getTextFromJsonBody("/reason"), equalTo("Invalid configuration")); + assertThat(response.getBody(), response.getTextFromJsonBody("/invalid_keys/keys"), equalTo(expectedInvalidKeys)); + } + + void assertSpecifyOneOf(final TestRestClient.HttpResponse response, final String expectedSpecifyOneOfKeys) { + assertThat(response.getBody(), response.getTextFromJsonBody("/reason"), equalTo("Invalid configuration")); + assertThat(response.getBody(), response.getTextFromJsonBody("/specify_one_of/keys"), containsString(expectedSpecifyOneOfKeys)); + } + + void assertNullValuesInArray(CheckedSupplier endpointCallback) throws Exception { + final var response = endpointCallback.get(); + assertThat(response.getBody(), response.getTextFromJsonBody("/reason"), equalTo("`null` is not allowed as json array element")); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java new file mode 100644 index 0000000000..7fa298c1e4 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/AccountRestApiIntegrationTest.java @@ -0,0 +1,174 @@ +/* + * 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.api; + +import org.junit.Test; + +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.apache.commons.lang3.RandomStringUtils.randomAlphabetic; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.not; +import static org.opensearch.security.dlic.rest.support.Utils.hash; + +public class AccountRestApiIntegrationTest extends AbstractApiIntegrationTest { + + private final static String TEST_USER = "test-user"; + + private final static String RESERVED_USER = "reserved-user"; + + private final static String HIDDEN_USERS = "hidden-user"; + + public final static String TEST_USER_PASSWORD = randomAlphabetic(10); + + public final static String TEST_USER_NEW_PASSWORD = randomAlphabetic(10); + + static { + testSecurityConfig.user(new TestSecurityConfig.User(TEST_USER).password(TEST_USER_PASSWORD)) + .user(new TestSecurityConfig.User(RESERVED_USER).reserved(true)) + .user(new TestSecurityConfig.User(HIDDEN_USERS).hidden(true)); + } + + private String accountPath() { + return super.apiPath("account"); + } + + @Test + public void accountInfo() throws Exception { + withUser(NEW_USER, client -> { + var response = ok(() -> client.get(accountPath())); + final var account = response.bodyAsJsonNode(); + assertThat(response.getBody(), account.get("user_name").asText(), is(NEW_USER)); + assertThat(response.getBody(), not(account.get("is_reserved").asBoolean())); + assertThat(response.getBody(), not(account.get("is_hidden").asBoolean())); + assertThat(response.getBody(), account.get("is_internal_user").asBoolean()); + assertThat(response.getBody(), account.get("user_requested_tenant").isNull()); + assertThat(response.getBody(), account.get("backend_roles").isArray()); + assertThat(response.getBody(), account.get("custom_attribute_names").isArray()); + assertThat(response.getBody(), account.get("tenants").isObject()); + assertThat(response.getBody(), account.get("roles").isArray()); + }); + withUser(NEW_USER, "a", client -> unauthorized(() -> client.get(accountPath()))); + withUser("a", "b", client -> unauthorized(() -> client.get(accountPath()))); + } + + @Test + public void changeAccountPassword() throws Exception { + withUser(TEST_USER, TEST_USER_PASSWORD, this::verifyWrongPayload); + verifyPasswordCanBeChanged(); + + withUser(RESERVED_USER, client -> { + var response = ok(() -> client.get(accountPath())); + assertThat(response.getBody(), response.getBooleanFromJsonBody("/is_reserved")); + forbidden(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); + }); + withUser(HIDDEN_USERS, client -> { + var response = ok(() -> client.get(accountPath())); + assertThat(response.getBody(), response.getBooleanFromJsonBody("/is_hidden")); + notFound(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + ok(() -> client.get(accountPath())); + notFound(() -> client.putJson(accountPath(), changePasswordPayload(DEFAULT_PASSWORD, randomAlphabetic(10)))); + }); + } + + private void verifyWrongPayload(final TestRestClient client) throws Exception { + badRequest(() -> client.putJson(accountPath(), EMPTY_BODY)); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload(null, "new_password"))); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload("wrong-password", "some_new_pwd"))); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, null))); + badRequest(() -> client.putJson(accountPath(), changePasswordPayload(TEST_USER_PASSWORD, ""))); + badRequest(() -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, null))); + badRequest(() -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, ""))); + badRequest( + () -> client.putJson( + accountPath(), + (builder, params) -> builder.startObject() + .field("current_password", TEST_USER_PASSWORD) + .startArray("backend_roles") + .endArray() + .endObject() + ) + ); + } + + private void verifyPasswordCanBeChanged() throws Exception { + final var newPassword = randomAlphabetic(10); + withUser( + TEST_USER, + TEST_USER_PASSWORD, + client -> ok( + () -> client.putJson(accountPath(), changePasswordWithHashPayload(TEST_USER_PASSWORD, hash(newPassword.toCharArray()))) + ) + ); + withUser( + TEST_USER, + newPassword, + client -> ok(() -> client.putJson(accountPath(), changePasswordPayload(newPassword, TEST_USER_NEW_PASSWORD))) + ); + } + + @Test + public void testPutAccountRetainsAccountInformation() throws Exception { + final var username = "test"; + final String password = randomAlphabetic(10); + final String newPassword = randomAlphabetic(10); + withUser( + ADMIN_USER_NAME, + client -> created( + () -> client.putJson( + apiPath("internalusers", username), + (builder, params) -> builder.startObject() + .field("password", password) + .field("backend_roles") + .startArray() + .value("test-backend-role") + .endArray() + .field("opendistro_security_roles") + .startArray() + .value("user_limited-user__limited-role") + .endArray() + .field("attributes") + .startObject() + .field("foo", "bar") + .endObject() + .endObject() + ) + ) + ); + withUser(username, password, client -> ok(() -> client.putJson(accountPath(), changePasswordPayload(password, newPassword)))); + withUser(ADMIN_USER_NAME, client -> { + final var response = ok(() -> client.get(apiPath("internalusers", username))); + final var user = response.bodyAsJsonNode().get(username); + assertThat(user.toPrettyString(), user.get("backend_roles").get(0).asText(), is("test-backend-role")); + assertThat(user.toPrettyString(), user.get("opendistro_security_roles").get(0).asText(), is("user_limited-user__limited-role")); + assertThat(user.toPrettyString(), user.get("attributes").get("foo").asText(), is("bar")); + }); + } + + private ToXContentObject changePasswordPayload(final String currentPassword, final String newPassword) { + return (builder, params) -> { + builder.startObject(); + if (currentPassword != null) builder.field("current_password", currentPassword); + if (newPassword != null) builder.field("password", newPassword); + return builder.endObject(); + }; + } + + private ToXContentObject changePasswordWithHashPayload(final String currentPassword, final String hash) { + return (builder, params) -> builder.startObject().field("current_password", currentPassword).field("hash", hash).endObject(); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java b/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java index 44f8dca20b..8a7795e90f 100644 --- a/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/CreateResetPasswordTest.java @@ -63,7 +63,7 @@ public class CreateResetPasswordTest { SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, - true, + false, ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, CUSTOM_PASSWORD_REGEX, ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, diff --git a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java index 8bfcd3b8a8..635d9ecff4 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoTest.java @@ -11,46 +11,40 @@ package org.opensearch.security.api; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.http.HttpStatus; -import org.junit.ClassRule; +import java.util.List; + import org.junit.Test; -import org.junit.runner.RunWith; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.ClusterManager; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; import static org.opensearch.security.rest.DashboardsInfoAction.DEFAULT_PASSWORD_MESSAGE; import static org.opensearch.security.rest.DashboardsInfoAction.DEFAULT_PASSWORD_REGEX; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class DashboardsInfoTest { +public class DashboardsInfoTest extends AbstractApiIntegrationTest { - protected final static TestSecurityConfig.User DASHBOARDS_USER = new TestSecurityConfig.User("dashboards_user").roles( - new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") - ); + static { + testSecurityConfig.user( + new TestSecurityConfig.User("dashboards_user").roles( + new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") + ) + ); + } - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(DASHBOARDS_USER) - .build(); + private String apiPath() { + return randomFrom(List.of(PLUGINS_PREFIX + "/dashboardsinfo", LEGACY_OPENDISTRO_PREFIX + "/kibanainfo")); + } @Test public void testDashboardsInfoValidationMessage() throws Exception { - - try (TestRestClient client = cluster.getRestClient(DASHBOARDS_USER)) { - TestRestClient.HttpResponse response = client.get("_plugins/_security/dashboardsinfo"); - assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + withUser("dashboards_user", client -> { + final var response = ok(() -> client.get(apiPath())); assertThat(response.getTextFromJsonBody("/password_validation_error_message"), equalTo(DEFAULT_PASSWORD_MESSAGE)); assertThat(response.getTextFromJsonBody("/password_validation_regex"), equalTo(DEFAULT_PASSWORD_REGEX)); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java index 6e4444d049..96aed9ddd8 100644 --- a/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java +++ b/src/integrationTest/java/org/opensearch/security/api/DashboardsInfoWithSettingsTest.java @@ -11,60 +11,47 @@ package org.opensearch.security.api; -import java.util.Map; +import java.util.List; -import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.http.HttpStatus; -import org.junit.ClassRule; import org.junit.Test; -import org.junit.runner.RunWith; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; -import org.opensearch.test.framework.cluster.ClusterManager; -import org.opensearch.test.framework.cluster.LocalCluster; -import org.opensearch.test.framework.cluster.TestRestClient; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; -import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) -@ThreadLeakScope(ThreadLeakScope.Scope.NONE) -public class DashboardsInfoWithSettingsTest { +public class DashboardsInfoWithSettingsTest extends AbstractApiIntegrationTest { - protected final static TestSecurityConfig.User DASHBOARDS_USER = new TestSecurityConfig.User("dashboards_user").roles( - new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") - ); + private static final String CUSTOM_PASSWORD_REGEX = "(?=.*[A-Z])(?=.*[^a-zA-Z\\d])(?=.*[0-9])(?=.*[a-z]).{5,}"; private static final String CUSTOM_PASSWORD_MESSAGE = "Password must be minimum 5 characters long and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character."; - private static final String CUSTOM_PASSWORD_REGEX = "(?=.*[A-Z])(?=.*[^a-zA-Z\\d])(?=.*[0-9])(?=.*[a-z]).{5,}"; - - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(DASHBOARDS_USER) - .nodeSettings( - Map.of( - ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, - CUSTOM_PASSWORD_REGEX, - ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, - CUSTOM_PASSWORD_MESSAGE + static { + clusterSettings.put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, CUSTOM_PASSWORD_REGEX) + .put(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, CUSTOM_PASSWORD_MESSAGE); + testSecurityConfig.user( + new TestSecurityConfig.User("dashboards_user").roles( + new Role("dashboards_role").indexPermissions("read").on("*").clusterPermissions("cluster_composite_ops") ) - ) - .build(); + ); + } + + private String apiPath() { + return randomFrom(List.of(PLUGINS_PREFIX + "/dashboardsinfo", LEGACY_OPENDISTRO_PREFIX + "/kibanainfo")); + } @Test public void testDashboardsInfoValidationMessageWithCustomMessage() throws Exception { - try (TestRestClient client = cluster.getRestClient(DASHBOARDS_USER)) { - TestRestClient.HttpResponse response = client.get("_plugins/_security/dashboardsinfo"); - assertThat(response.getStatusCode(), equalTo(HttpStatus.SC_OK)); + withUser("dashboards_user", client -> { + final var response = ok(() -> client.get(apiPath())); assertThat(response.getTextFromJsonBody("/password_validation_error_message"), equalTo(CUSTOM_PASSWORD_MESSAGE)); assertThat(response.getTextFromJsonBody("/password_validation_regex"), equalTo(CUSTOM_PASSWORD_REGEX)); - } + }); } } diff --git a/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.java new file mode 100644 index 0000000000..eaecb275db --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/DefaultApiAvailabilityIntegrationTest.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.api; + +import org.junit.Test; + +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.Is.is; + +public class DefaultApiAvailabilityIntegrationTest extends AbstractApiIntegrationTest { + + @Test + public void nodesDnApiIsNotAvailableByDefault() throws Exception { + withUser(NEW_USER, this::verifyNodesDnApi); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyNodesDnApi); + } + + private void verifyNodesDnApi(final TestRestClient client) throws Exception { + badRequest(() -> client.get(apiPath("nodesdn"))); + badRequest(() -> client.putJson(apiPath("nodesdn", "cluster_1"), EMPTY_BODY)); + badRequest(() -> client.delete(apiPath("nodesdn", "cluster_1"))); + badRequest(() -> client.patch(apiPath("nodesdn", "cluster_1"), EMPTY_BODY)); + } + + @Test + public void securityConfigIsNotAvailableByDefault() throws Exception { + withUser(NEW_USER, client -> { + forbidden(() -> client.get(apiPath("securityconfig"))); + verifySecurityConfigApi(client); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + ok(() -> client.get(apiPath("securityconfig"))); + verifySecurityConfigApi(client); + }); + } + + private void verifySecurityConfigApi(final TestRestClient client) throws Exception { + methodNotAllowed(() -> client.putJson(apiPath("securityconfig"), EMPTY_BODY)); + methodNotAllowed(() -> client.postJson(apiPath("securityconfig"), EMPTY_BODY)); + methodNotAllowed(() -> client.delete(apiPath("securityconfig"))); + forbidden( + () -> client.patch( + apiPath("securityconfig"), + (builder, params) -> builder.startArray() + .startObject() + .field("op", "replace") + .field("path", "/a/b/c") + .field("value", "other") + .endObject() + .endArray() + ) + ); + } + + @Test + public void securityHealth() throws Exception { + withUser(NEW_USER, client -> ok(() -> client.get(securityPath("health")))); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> ok(() -> client.get(securityPath("health")))); + } + + @Test + public void securityAuthInfo() throws Exception { + withUser(NEW_USER, this::verifyAuthInfoApi); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifyAuthInfoApi); + } + + private void verifyAuthInfoApi(final TestRestClient client) throws Exception { + final var verbose = randomBoolean(); + + final TestRestClient.HttpResponse response; + if (verbose) response = ok(() -> client.get(securityPath("authinfo?verbose=" + verbose))); + else response = ok(() -> client.get(securityPath("authinfo"))); + final var body = response.bodyAsJsonNode(); + assertThat(response.getBody(), body.has("user")); + assertThat(response.getBody(), body.has("user_name")); + assertThat(response.getBody(), body.has("user_requested_tenant")); + assertThat(response.getBody(), body.has("remote_address")); + assertThat(response.getBody(), body.has("backend_roles")); + assertThat(response.getBody(), body.has("custom_attribute_names")); + assertThat(response.getBody(), body.has("roles")); + assertThat(response.getBody(), body.has("tenants")); + assertThat(response.getBody(), body.has("principal")); + assertThat(response.getBody(), body.has("peer_certificates")); + assertThat(response.getBody(), body.has("sso_logout_url")); + + if (verbose) { + assertThat(response.getBody(), body.has("size_of_user")); + assertThat(response.getBody(), body.has("size_of_custom_attributes")); + assertThat(response.getBody(), body.has("size_of_backendroles")); + } + + } + + @Test + public void flushCache() throws Exception { + withUser(NEW_USER, client -> { + forbidden(() -> client.get(apiPath("cache"))); + forbidden(() -> client.postJson(apiPath("cache"), EMPTY_BODY)); + forbidden(() -> client.putJson(apiPath("cache"), EMPTY_BODY)); + forbidden(() -> client.delete(apiPath("cache"))); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + notImplemented(() -> client.get(apiPath("cache"))); + notImplemented(() -> client.postJson(apiPath("cache"), EMPTY_BODY)); + notImplemented(() -> client.putJson(apiPath("cache"), EMPTY_BODY)); + final var response = ok(() -> client.delete(apiPath("cache"))); + assertThat(response.getBody(), response.getTextFromJsonBody("/message"), is("Cache flushed successfully.")); + }); + } + + @Test + public void reloadSSLCertsNotAvailable() throws Exception { + withUser(NEW_USER, client -> { + forbidden(() -> client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY)); + forbidden(() -> client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY)); + }); + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), client -> { + badRequest(() -> client.putJson(apiPath("ssl", "http", "reloadcerts"), EMPTY_BODY)); + badRequest(() -> client.putJson(apiPath("ssl", "transport", "reloadcerts"), EMPTY_BODY)); + }); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java new file mode 100644 index 0000000000..61085d3f8a --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/api/SslCertsRestApiIntegrationTest.java @@ -0,0 +1,79 @@ +/* + * 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.api; + +import com.fasterxml.jackson.databind.JsonNode; +import org.junit.Test; + +import org.opensearch.security.dlic.rest.api.Endpoint; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.CERTS_INFO_ACTION; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; + +public class SslCertsRestApiIntegrationTest extends AbstractApiIntegrationTest { + + final static String REST_API_ADMIN_SSL_INFO = "rest-api-admin-ssl-info"; + + static { + clusterSettings.put(SECURITY_RESTAPI_ADMIN_ENABLED, true); + testSecurityConfig.withRestAdminUser(REST_ADMIN_USER, allRestAdminPermissions()) + .withRestAdminUser(REST_API_ADMIN_SSL_INFO, restAdminPermission(Endpoint.SSL, CERTS_INFO_ACTION)); + } + + protected String sslCertsPath() { + return super.apiPath("ssl", "certs"); + } + + @Test + public void certsInfoForbiddenForRegularUser() throws Exception { + withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath()))); + } + + @Test + public void certsInfoForbiddenForAdminUser() throws Exception { + withUser(NEW_USER, client -> forbidden(() -> client.get(sslCertsPath()))); + } + + @Test + public void certsInfoAvailableForTlsAdmin() throws Exception { + withUser(ADMIN_USER_NAME, localCluster.getAdminCertificate(), this::verifySSLCertsInfo); + } + + @Test + public void certsInfoAvailableForRestAdmin() throws Exception { + withUser(REST_ADMIN_USER, this::verifySSLCertsInfo); + withUser(REST_API_ADMIN_SSL_INFO, this::verifySSLCertsInfo); + } + + private void verifySSLCertsInfo(final TestRestClient client) throws Exception { + final var response = ok(() -> client.get(sslCertsPath())); + + final var body = response.bodyAsJsonNode(); + assertThat(response.getBody(), body.has("http_certificates_list")); + assertThat(response.getBody(), body.get("http_certificates_list").isArray()); + verifyCertsJson(body.get("http_certificates_list").get(0)); + assertThat(response.getBody(), body.has("transport_certificates_list")); + assertThat(response.getBody(), body.get("transport_certificates_list").isArray()); + verifyCertsJson(body.get("transport_certificates_list").get(0)); + } + + private void verifyCertsJson(final JsonNode jsonNode) { + assertThat(jsonNode.toPrettyString(), jsonNode.has("issuer_dn")); + assertThat(jsonNode.toPrettyString(), jsonNode.has("subject_dn")); + assertThat(jsonNode.toPrettyString(), jsonNode.get("subject_dn").asText().matches(".*node-\\d.example.com+")); + assertThat(jsonNode.toPrettyString(), jsonNode.get("san").asText().matches(".*node-\\d.example.com.*")); + assertThat(jsonNode.toPrettyString(), jsonNode.has("not_before")); + assertThat(jsonNode.toPrettyString(), jsonNode.has("not_after")); + } + +} diff --git a/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java index b1c13aeedc..99f96a388e 100644 --- a/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java @@ -17,7 +17,6 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -45,9 +44,9 @@ public class AnonymousAuthenticationTest { /** * Maps {@link #ANONYMOUS_USER_CUSTOM_ROLE} to {@link #DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME} */ - private static final RolesMapping ANONYMOUS_USER_CUSTOM_ROLE_MAPPING = new RolesMapping(ANONYMOUS_USER_CUSTOM_ROLE).backendRoles( - DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME - ); + private static final TestSecurityConfig.RoleMapping ANONYMOUS_USER_CUSTOM_ROLE_MAPPING = new TestSecurityConfig.RoleMapping( + ANONYMOUS_USER_CUSTOM_ROLE.getName() + ).backendRoles(DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME); /** * User who is stored in the internal user database and can authenticate diff --git a/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java b/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java index 16ebd29885..514d2c45d1 100644 --- a/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/AsyncTests.java @@ -18,7 +18,7 @@ import java.util.concurrent.TimeUnit; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; @@ -26,7 +26,6 @@ import org.opensearch.security.IndexOperationsHelper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.test.framework.AsyncActions; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -44,7 +43,7 @@ public class AsyncTests { public static LocalCluster cluster = new LocalCluster.Builder().singleNode() .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) - .rolesMapping(new RolesMapping(ALL_ACCESS).backendRoles("admin")) + .rolesMapping(new TestSecurityConfig.RoleMapping(ALL_ACCESS.getName()).backendRoles("admin")) .anonymousAuth(false) .nodeSettings(Map.of(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED, List.of(ALL_ACCESS.getName()))) .build(); diff --git a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java index 1e424ab115..a9888d281e 100644 --- a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java @@ -106,6 +106,19 @@ public void testBrowserShouldRequestForCredentials() { } } + @Test + public void shouldRespondWithChallengeWhenNoCredentialsArePresent() { + try (TestRestClient client = cluster.getRestClient()) { + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + assertThat(response.getHeader("WWW-Authenticate"), is(notNullValue())); + assertThat(response.getHeader("WWW-Authenticate").getValue(), equalTo("Basic realm=\"OpenSearch Security\"")); + assertThat(response.getBody(), equalTo("Unauthorized")); + } + } + @Test public void testUserShouldNotHaveAssignedCustomAttributes() { try (TestRestClient client = cluster.getRestClient(TEST_USER)) { diff --git a/src/integrationTest/java/org/opensearch/security/http/BasicWithAnonymousAuthTests.java b/src/integrationTest/java/org/opensearch/security/http/BasicWithAnonymousAuthTests.java new file mode 100644 index 0000000000..842d5c4dd5 --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/BasicWithAnonymousAuthTests.java @@ -0,0 +1,112 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +* +*/ +package org.opensearch.security.http; + +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; +import org.opensearch.test.framework.TestSecurityConfig.User; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.apache.http.HttpStatus.SC_UNAUTHORIZED; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class BasicWithAnonymousAuthTests { + static final User TEST_USER = new User("test_user").password("s3cret"); + + public static final String CUSTOM_ATTRIBUTE_NAME = "superhero"; + static final User SUPER_USER = new User("super-user").password("super-password").attr(CUSTOM_ATTRIBUTE_NAME, "true"); + public static final String NOT_EXISTING_USER = "not-existing-user"; + public static final String INVALID_PASSWORD = "secret-password"; + + public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0).httpAuthenticatorWithChallenge("basic").backend("internal"); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(true) + .authc(AUTHC_DOMAIN) + .users(TEST_USER, SUPER_USER) + .build(); + + /** No automatic login post anonymous auth request **/ + @Test + public void testShouldRespondWith401WhenUserDoesNotExist() { + try (TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER, INVALID_PASSWORD)) { + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void testShouldRespondWith401WhenUserNameIsIncorrect() { + try (TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER, TEST_USER.getPassword())) { + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void testShouldRespondWith401WhenPasswordIsIncorrect() { + try (TestRestClient client = cluster.getRestClient(TEST_USER.getName(), INVALID_PASSWORD)) { + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + /** Test `?auth_type=""` param to authinfo request **/ + @Test + public void testShouldAutomaticallyLoginAsAnonymousIfNoCredentialsArePassed() { + try (TestRestClient client = cluster.getRestClient()) { + + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_OK); + + HttpResponse response2 = client.getAuthInfo(Map.of("auth_type", "anonymous")); + + assertThat(response2, is(notNullValue())); + response2.assertStatusCode(SC_OK); + } + } + + @Test + public void testShouldNotAutomaticallyLoginAsAnonymousIfRequestIsNonAnonymousLogin() { + try (TestRestClient client = cluster.getRestClient()) { + + HttpResponse response = client.getAuthInfo(Map.of("auth_type", "saml")); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + + // should contain a redirect link + assertThat(response.containHeader("WWW-Authenticate"), is(true)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java index 975ce25efb..18ae232a91 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java @@ -17,7 +17,7 @@ import org.junit.Test; import org.junit.runner.RunWith; -import org.opensearch.test.framework.RolesMapping; +import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; import org.opensearch.test.framework.TestSecurityConfig.Role; @@ -69,7 +69,7 @@ public class CertificateAuthenticationTest { .authc(AUTHC_HTTPBASIC_INTERNAL) .roles(ROLE_ALL_INDEX_SEARCH) .users(USER_ADMIN) - .rolesMapping(new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles(BACKEND_ROLE_BRIDGE)) + .rolesMapping(new TestSecurityConfig.RoleMapping(ROLE_ALL_INDEX_SEARCH.getName()).backendRoles(BACKEND_ROLE_BRIDGE)) .build(); private static final TestCertificates TEST_CERTIFICATES = cluster.getTestCertificates(); diff --git a/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java index 49ded4f2a9..48ed08ac22 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java @@ -13,7 +13,6 @@ import java.net.InetAddress; import java.util.List; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.LocalCluster; import org.opensearch.test.framework.cluster.TestRestClient; @@ -31,7 +30,7 @@ */ abstract class CommonProxyAuthenticationTests { - protected static final String RESOURCE_AUTH_INFO = "/_opendistro/_security/authinfo"; + protected static final String RESOURCE_AUTH_INFO = "_opendistro/_security/authinfo"; protected static final TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); protected static final String ATTRIBUTE_DEPARTMENT = "department"; @@ -84,13 +83,13 @@ abstract class CommonProxyAuthenticationTests { .indexPermissions("indices:data/read/search") .on(PERSONAL_INDEX_NAME_PATTERN); - protected static final RolesMapping ROLES_MAPPING_CAPTAIN = new RolesMapping(ROLE_PERSONAL_INDEX_SEARCH).backendRoles( - BACKEND_ROLE_CAPTAIN - ); + protected static final TestSecurityConfig.RoleMapping ROLES_MAPPING_CAPTAIN = new TestSecurityConfig.RoleMapping( + ROLE_PERSONAL_INDEX_SEARCH.getName() + ).backendRoles(BACKEND_ROLE_CAPTAIN); - protected static final RolesMapping ROLES_MAPPING_FIRST_MATE = new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles( - BACKEND_ROLE_FIRST_MATE - ); + protected static final TestSecurityConfig.RoleMapping ROLES_MAPPING_FIRST_MATE = new TestSecurityConfig.RoleMapping( + ROLE_ALL_INDEX_SEARCH.getName() + ).backendRoles(BACKEND_ROLE_FIRST_MATE); protected abstract LocalCluster getCluster(); diff --git a/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java index 6fcc7eac83..7c361828d6 100644 --- a/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java @@ -230,7 +230,7 @@ public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPos .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); try (TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { - HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_SPOCK + "/_search"); + HttpResponse response = client.get(PERSONAL_INDEX_NAME_SPOCK + "/_search"); response.assertStatusCode(200); assertThat(response.getLongFromJsonBody(POINTER_TOTAL_HITS), equalTo(1L)); @@ -251,7 +251,7 @@ public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPos .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); try (TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { - HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_KIRK + "/_search"); + HttpResponse response = client.get(PERSONAL_INDEX_NAME_KIRK + "/_search"); response.assertStatusCode(403); } diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java new file mode 100644 index 0000000000..e10ad82e8c --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationWithUrlParamTests.java @@ -0,0 +1,126 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.http; + +import java.security.KeyPair; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.Header; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.AuditCompliance; +import org.opensearch.test.framework.AuditConfiguration; +import org.opensearch.test.framework.AuditFilters; +import org.opensearch.test.framework.JwtConfigBuilder; +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.audit.AuditLogsRule; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; +import org.opensearch.test.framework.log.LogsRule; + +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; + +import static java.nio.charset.StandardCharsets.US_ASCII; +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; +import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; +import static org.opensearch.test.framework.audit.AuditMessagePredicate.userAuthenticated; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class JwtAuthenticationWithUrlParamTests { + + public static final String CLAIM_USERNAME = "preferred-username"; + public static final String CLAIM_ROLES = "backend-user-roles"; + public static final String POINTER_USERNAME = "/user_name"; + + private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final String TOKEN_URL_PARAM = "token"; + + private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( + KEY_PAIR.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + AUTHORIZATION + ); + + public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain( + "jwt", + BASIC_AUTH_DOMAIN_ORDER - 1 + ).jwtHttpAuthenticator( + new JwtConfigBuilder().jwtUrlParameter(TOKEN_URL_PARAM).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) + ).backend("noop"); + + @Rule + public AuditLogsRule auditLogsRule = new AuditLogsRule(); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .nodeSettings( + Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName())) + ) + .audit( + new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true)) + .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .authc(JWT_AUTH_DOMAIN) + .users(ADMIN_USER) + .build(); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); + + @Test + public void shouldAuthenticateWithJwtTokenInUrl_positive() { + Header jwtToken = tokenFactory.generateValidToken(ADMIN_USER.getName()); + String jwtTokenValue = jwtToken.getValue(); + try (TestRestClient client = cluster.getRestClient()) { + HttpResponse response = client.getAuthInfo(Map.of(TOKEN_URL_PARAM, jwtTokenValue, "verbose", "true")); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(ADMIN_USER.getName())); + Map expectedParams = Map.of("token", "REDACTED", "verbose", "true"); + + auditLogsRule.assertExactlyOne( + userAuthenticated(ADMIN_USER).withRestRequest(GET, "/_opendistro/_security/authinfo").withRestParams(expectedParams) + ); + } + } + + @Test + public void testUnauthenticatedRequest() { + try (TestRestClient client = cluster.getRestClient()) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' url parameter header", TOKEN_URL_PARAM)); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java index 299b2cc7d2..090762af21 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java @@ -10,8 +10,10 @@ package org.opensearch.security.http; import java.util.List; +import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.hc.core5.http.message.BasicHeader; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.ClassRule; @@ -20,7 +22,11 @@ import org.junit.rules.RuleChain; import org.junit.runner.RunWith; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.test.framework.AuthorizationBackend; +import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; +import org.opensearch.test.framework.LdapAuthorizationConfigBuilder; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; @@ -32,15 +38,25 @@ import org.opensearch.test.framework.ldap.EmbeddedLDAPServer; import org.opensearch.test.framework.log.LogsRule; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.security.http.CertificateAuthenticationTest.POINTER_BACKEND_ROLES; +import static org.opensearch.security.http.DirectoryInformationTrees.CN_GROUP_ADMIN; import static org.opensearch.security.http.DirectoryInformationTrees.DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG; +import static org.opensearch.security.http.DirectoryInformationTrees.DN_GROUPS_TEST_ORG; import static org.opensearch.security.http.DirectoryInformationTrees.DN_OPEN_SEARCH_PEOPLE_TEST_ORG; import static org.opensearch.security.http.DirectoryInformationTrees.DN_PEOPLE_TEST_ORG; import static org.opensearch.security.http.DirectoryInformationTrees.LDIF_DATA; +import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_KIRK; import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_OPEN_SEARCH; import static org.opensearch.security.http.DirectoryInformationTrees.PASSWORD_SPOCK; import static org.opensearch.security.http.DirectoryInformationTrees.USERNAME_ATTRIBUTE; +import static org.opensearch.security.http.DirectoryInformationTrees.USER_KIRK; import static org.opensearch.security.http.DirectoryInformationTrees.USER_SEARCH; import static org.opensearch.security.http.DirectoryInformationTrees.USER_SPOCK; +import static org.opensearch.security.support.ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.BASIC_AUTH_DOMAIN_ORDER; import static org.opensearch.test.framework.TestSecurityConfig.Role.ALL_ACCESS; @@ -54,6 +70,8 @@ public class LdapAuthenticationTest { private static final Logger log = LogManager.getLogger(LdapAuthenticationTest.class); + private static final String HEADER_NAME_IMPERSONATE = "opendistro_security_impersonate_as"; + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); @@ -67,6 +85,16 @@ public class LdapAuthenticationTest { public static LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES) .clusterManager(ClusterManager.SINGLENODE) .anonymousAuth(false) + .nodeSettings( + Map.of( + ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + ADMIN_USER.getName(), + List.of(USER_KIRK), + SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, + false + ) + ) .authc( new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) .backend( @@ -89,6 +117,28 @@ public class LdapAuthenticationTest { ) .authc(AUTHC_HTTPBASIC_INTERNAL) .users(ADMIN_USER) + .rolesMapping(new TestSecurityConfig.RoleMapping(ALL_ACCESS.getName()).backendRoles(CN_GROUP_ADMIN)) + .authz( + new AuthzDomain("ldap_roles").httpEnabled(true) + .authorizationBackend( + new AuthorizationBackend("ldap").config( + () -> new LdapAuthorizationConfigBuilder().hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) + .enableSsl(false) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .roleBase(DN_GROUPS_TEST_ORG) + .roleSearch("(uniqueMember={0})") + .userRoleAttribute(null) + .userRoleName("disabled") + .roleName("cn") + .resolveNestedRoles(true) + .build() + ) + ) + ) .build(); @ClassRule @@ -117,4 +167,49 @@ public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { logsRule.assertThatStackTraceContain(expectedStackTraceFragment); } } + + @Test + public void testShouldCreateScrollWithLdapUserAndImpersonateWithAdmin() { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + TestRestClient.HttpResponse response = client.put("movies"); + + response.assertStatusCode(200); + } + + String scrollId; + + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + TestRestClient.HttpResponse authinfo = client.getAuthInfo(); + + List backendRoles = authinfo.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, contains(CN_GROUP_ADMIN)); + + TestRestClient.HttpResponse response = client.getWithJsonBody("movies/_search?scroll=10m", "{\"size\": 1}"); + + response.assertStatusCode(200); + + scrollId = response.getTextFromJsonBody("/_scroll_id"); + } + + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + TestRestClient.HttpResponse scrollResponse = client.getWithJsonBody( + "_search/scroll", + "{\"scroll\": \"10m\", \"scroll_id\": \"" + scrollId + "\"}", + new BasicHeader(HEADER_NAME_IMPERSONATE, USER_KIRK) + ); + + scrollResponse.assertStatusCode(200); + } + } + + @Test + public void testShouldRedactPasswordWhenGettingSecurityConfig() { + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + TestRestClient.HttpResponse response = client.get("_plugins/_security/api/securityconfig"); + + response.assertStatusCode(200); + String redactedPassword = response.getTextFromJsonBody("/config/dynamic/authc/ldap/authentication_backend/config/password"); + assertThat("******", equalTo(redactedPassword)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java index bac79ffd12..e5c1012bdc 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java @@ -30,7 +30,7 @@ import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.LdapAuthenticationConfigBuilder; import org.opensearch.test.framework.LdapAuthorizationConfigBuilder; -import org.opensearch.test.framework.RolesMapping; +import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AuthenticationBackend; import org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.HttpAuthenticator; @@ -151,12 +151,11 @@ public class LdapTlsAuthenticationTest { .users(ADMIN_USER) .roles(ROLE_INDEX_ADMINISTRATOR, ROLE_PERSONAL_INDEX_ACCESS) .rolesMapping( - new RolesMapping(ROLE_INDEX_ADMINISTRATOR).backendRoles(CN_GROUP_ADMIN), - new RolesMapping(ROLE_PERSONAL_INDEX_ACCESS).backendRoles(CN_GROUP_CREW) + new TestSecurityConfig.RoleMapping(ROLE_INDEX_ADMINISTRATOR.getName()).backendRoles(CN_GROUP_ADMIN), + new TestSecurityConfig.RoleMapping(ROLE_PERSONAL_INDEX_ACCESS.getName()).backendRoles(CN_GROUP_CREW) ) .authz( new AuthzDomain("ldap_roles").httpEnabled(true) - .transportEnabled(true) .authorizationBackend( new AuthorizationBackend("ldap").config( () -> new LdapAuthorizationConfigBuilder().hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) diff --git a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java index 2b56573dfe..1dbb10b1f8 100644 --- a/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/OnBehalfOfJwtAuthenticationTest.java @@ -22,8 +22,8 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; @@ -33,7 +33,6 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.authtoken.jwt.EncryptionDecryptionUtil; import org.opensearch.test.framework.OnBehalfOfConfig; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.cluster.ClusterManager; import org.opensearch.test.framework.cluster.LocalCluster; @@ -48,7 +47,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; -import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED; import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; @@ -128,8 +127,8 @@ private static OnBehalfOfConfig defaultOnBehalfOfConfig() { .users(ADMIN_USER, OBO_USER, OBO_USER_NO_PERM, HOST_MAPPING_OBO_USER) .nodeSettings( Map.of( - SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, - true, + SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, + false, SECURITY_RESTAPI_ROLES_ENABLED, ADMIN_USER.getRoleNames(), SECURITY_RESTAPI_ADMIN_ENABLED, @@ -139,7 +138,7 @@ private static OnBehalfOfConfig defaultOnBehalfOfConfig() { ) ) .authc(AUTHC_HTTPBASIC_INTERNAL) - .rolesMapping(new RolesMapping(HOST_MAPPING_ROLE).hostIPs(HOST_MAPPING_IP)) + .rolesMapping(new TestSecurityConfig.RoleMapping(HOST_MAPPING_ROLE.getName()).hosts(HOST_MAPPING_IP)) .onBehalfOf(defaultOnBehalfOfConfig()) .build(); diff --git a/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java b/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java new file mode 100644 index 0000000000..828790e47b --- /dev/null +++ b/src/integrationTest/java/org/opensearch/security/http/RolesMappingTests.java @@ -0,0 +1,85 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.http; + +import java.util.List; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.TestSecurityConfig; +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; + +import static org.apache.http.HttpStatus.SC_OK; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class RolesMappingTests { + static final TestSecurityConfig.User USER_A = new TestSecurityConfig.User("userA").password("s3cret").backendRoles("mapsToRoleA"); + static final TestSecurityConfig.User USER_B = new TestSecurityConfig.User("userB").password("P@ssw0rd").backendRoles("mapsToRoleB"); + + private static final TestSecurityConfig.Role ROLE_A = new TestSecurityConfig.Role("roleA").clusterPermissions("cluster_all"); + + private static final TestSecurityConfig.Role ROLE_B = new TestSecurityConfig.Role("roleB").clusterPermissions("cluster_all"); + + public static final TestSecurityConfig.AuthcDomain AUTHC_DOMAIN = new TestSecurityConfig.AuthcDomain("basic", 0) + .httpAuthenticatorWithChallenge("basic") + .backend("internal"); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_DOMAIN) + .roles(ROLE_A, ROLE_B) + .rolesMapping( + new TestSecurityConfig.RoleMapping(ROLE_A.getName()).backendRoles("mapsToRoleA"), + new TestSecurityConfig.RoleMapping(ROLE_B.getName()).backendRoles("mapsToRoleB") + ) + .users(USER_A, USER_B) + .build(); + + @Test + public void testBackendRoleToRoleMapping() { + try (TestRestClient client = cluster.getRestClient(USER_A)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + List roles = response.getTextArrayFromJsonBody("/roles"); + List backendRoles = response.getTextArrayFromJsonBody("/backend_roles"); + assertThat(roles, contains(ROLE_A.getName())); + assertThat(roles, not(contains(ROLE_B.getName()))); + assertThat(backendRoles, contains("mapsToRoleA")); + response.assertStatusCode(SC_OK); + } + + try (TestRestClient client = cluster.getRestClient(USER_B)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + List roles = response.getTextArrayFromJsonBody("/roles"); + List backendRoles = response.getTextArrayFromJsonBody("/backend_roles"); + assertThat(roles, contains(ROLE_B.getName())); + assertThat(roles, not(contains(ROLE_A.getName()))); + assertThat(backendRoles, contains("mapsToRoleB")); + response.assertStatusCode(SC_OK); + } + } +} diff --git a/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java index 762feed686..932ca71e44 100644 --- a/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/ServiceAccountAuthenticationTest.java @@ -15,7 +15,7 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java index 2315c979ea..561b4a0742 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java @@ -18,6 +18,7 @@ import org.junit.runner.RunWith; import org.opensearch.script.mustache.MustacheModulePlugin; +import org.opensearch.script.mustache.RenderSearchTemplateAction; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; import org.opensearch.test.framework.cluster.ClusterManager; @@ -49,15 +50,25 @@ public class PrivilegesEvaluatorTest { new Role("search_template_role").indexPermissions("read").on("services").clusterPermissions("cluster_composite_ops") ); + protected final static TestSecurityConfig.User RENDER_SEARCH_TEMPLATE = new TestSecurityConfig.User("render_search_template_user") + .roles( + new Role("render_search_template_role").indexPermissions("read") + .on("services") + .clusterPermissions(RenderSearchTemplateAction.NAME) + ); + private String TEST_QUERY = "{\"source\":{\"query\":{\"match\":{\"service\":\"{{service_name}}\"}}},\"params\":{\"service_name\":\"Oracle\"}}"; private String TEST_DOC = "{\"source\": {\"title\": \"Spirited Away\"}}"; + private String TEST_RENDER_SEARCH_TEMPLATE_QUERY = + "{\"params\":{\"status\":[\"pending\",\"published\"]},\"source\":\"{\\\"query\\\": {\\\"terms\\\": {\\\"status\\\": [\\\"{{#status}}\\\",\\\"{{.}}\\\",\\\"{{/status}}\\\"]}}}\"}"; + @ClassRule public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX, SEARCH_TEMPLATE, TestSecurityConfig.User.USER_ADMIN) + .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX, SEARCH_TEMPLATE, RENDER_SEARCH_TEMPLATE, TestSecurityConfig.User.USER_ADMIN) .plugin(MustacheModulePlugin.class) .build(); @@ -118,4 +129,28 @@ public void testSearchTemplateRequestUnauthorizedAllIndices() { assertThat(searchOnAllIndicesResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); } } + + @Test + public void testRenderSearchTemplateRequestFailure() { + try (TestRestClient client = cluster.getRestClient(SEARCH_TEMPLATE)) { + final String renderSearchTemplate = "_render/template"; + final TestRestClient.HttpResponse renderSearchTemplateResponse = client.postJson( + renderSearchTemplate, + TEST_RENDER_SEARCH_TEMPLATE_QUERY + ); + assertThat(renderSearchTemplateResponse.getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + } + } + + @Test + public void testRenderSearchTemplateRequestSuccess() { + try (TestRestClient client = cluster.getRestClient(RENDER_SEARCH_TEMPLATE)) { + final String renderSearchTemplate = "_render/template"; + final TestRestClient.HttpResponse renderSearchTemplateResponse = client.postJson( + renderSearchTemplate, + TEST_RENDER_SEARCH_TEMPLATE_QUERY + ); + assertThat(renderSearchTemplateResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/rest/AuthZinRestLayerTests.java b/src/integrationTest/java/org/opensearch/security/rest/AuthZinRestLayerTests.java index ad13d69db7..c38ecbb611 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/AuthZinRestLayerTests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/AuthZinRestLayerTests.java @@ -12,7 +12,7 @@ package org.opensearch.security.rest; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; diff --git a/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java b/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java index 40c90764d9..bcbbc37400 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/CompressionTests.java @@ -20,9 +20,9 @@ import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.core5.http.ContentType; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.io.entity.ByteArrayEntity; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; diff --git a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java index 0324cd449d..c41b5f4cda 100644 --- a/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java +++ b/src/integrationTest/java/org/opensearch/security/rest/WhoAmITests.java @@ -22,7 +22,7 @@ import java.util.stream.Collectors; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -172,7 +172,7 @@ public void testAuditLogSimilarityWithTransportLayer() { assertThat(client.get("_cat/indices").getStatusCode(), equalTo(HttpStatus.SC_OK)); // transport layer audit messages - auditLogsRule.assertExactly(2, grantedPrivilege(AUDIT_LOG_VERIFIER, "GetSettingsRequest")); + auditLogsRule.assertExactly(1, grantedPrivilege(AUDIT_LOG_VERIFIER, "GetSettingsRequest")); List grantedPrivilegesMessages = auditLogsRule.getCurrentTestAuditMessages() .stream() diff --git a/src/integrationTest/java/org/opensearch/test/framework/AuditFilters.java b/src/integrationTest/java/org/opensearch/test/framework/AuditFilters.java index f984becefa..5e63665f41 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/AuditFilters.java +++ b/src/integrationTest/java/org/opensearch/test/framework/AuditFilters.java @@ -34,6 +34,9 @@ public class AuditFilters implements ToXContentObject { private List ignoreRequests; + private List ignoreHeaders; + private List ignoreUrlParams; + private List disabledRestCategories; private List disabledTransportCategories; @@ -49,6 +52,8 @@ public AuditFilters() { this.ignoreUsers = Collections.emptyList(); this.ignoreRequests = Collections.emptyList(); + this.ignoreHeaders = Collections.emptyList(); + this.ignoreUrlParams = Collections.emptyList(); this.disabledRestCategories = Collections.emptyList(); this.disabledTransportCategories = Collections.emptyList(); } @@ -93,6 +98,16 @@ public AuditFilters ignoreRequests(List ignoreRequests) { return this; } + public AuditFilters ignoreHeaders(List ignoreHeaders) { + this.ignoreHeaders = ignoreHeaders; + return this; + } + + public AuditFilters ignoreUrlParams(List ignoreUrlParams) { + this.ignoreUrlParams = ignoreUrlParams; + return this; + } + public AuditFilters disabledRestCategories(List disabledRestCategories) { this.disabledRestCategories = disabledRestCategories; return this; @@ -114,6 +129,8 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.field("exclude_sensitive_headers", excludeSensitiveHeaders); xContentBuilder.field("ignore_users", ignoreUsers); xContentBuilder.field("ignore_requests", ignoreRequests); + xContentBuilder.field("ignore_headers", ignoreHeaders); + xContentBuilder.field("ignore_url_params", ignoreUrlParams); xContentBuilder.field("disabled_rest_categories", disabledRestCategories); xContentBuilder.field("disabled_transport_categories", disabledTransportCategories); xContentBuilder.endObject(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java b/src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java index 5ccf1f9ee0..d56344d5d2 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java +++ b/src/integrationTest/java/org/opensearch/test/framework/AuthzDomain.java @@ -25,8 +25,6 @@ public class AuthzDomain implements ToXContentObject { private boolean httpEnabled; - private boolean transportEnabled; - private AuthorizationBackend authorizationBackend; public AuthzDomain(String id) { @@ -52,17 +50,11 @@ public AuthzDomain authorizationBackend(AuthorizationBackend authorizationBacken return this; } - public AuthzDomain transportEnabled(boolean transportEnabled) { - this.transportEnabled = transportEnabled; - return this; - } - @Override public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { xContentBuilder.startObject(); xContentBuilder.field("description", description); xContentBuilder.field("http_enabled", httpEnabled); - xContentBuilder.field("transport_enabled", transportEnabled); xContentBuilder.field("authorization_backend", authorizationBackend); xContentBuilder.endObject(); return xContentBuilder; diff --git a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java index 48dfa128e0..88297bacd2 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java +++ b/src/integrationTest/java/org/opensearch/test/framework/JwtConfigBuilder.java @@ -18,6 +18,7 @@ public class JwtConfigBuilder { private String jwtHeader; + private String jwtUrlParameter; private String signingKey; private String subjectKey; private String rolesKey; @@ -27,6 +28,11 @@ public JwtConfigBuilder jwtHeader(String jwtHeader) { return this; } + public JwtConfigBuilder jwtUrlParameter(String jwtUrlParameter) { + this.jwtUrlParameter = jwtUrlParameter; + return this; + } + public JwtConfigBuilder signingKey(String signingKey) { this.signingKey = signingKey; return this; @@ -51,6 +57,9 @@ public Map build() { if (isNoneBlank(jwtHeader)) { builder.put("jwt_header", jwtHeader); } + if (isNoneBlank(jwtUrlParameter)) { + builder.put("jwt_url_parameter", jwtUrlParameter); + } if (isNoneBlank(subjectKey)) { builder.put("subject_key", subjectKey); } diff --git a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java b/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java deleted file mode 100644 index 997e7e128b..0000000000 --- a/src/integrationTest/java/org/opensearch/test/framework/RolesMapping.java +++ /dev/null @@ -1,108 +0,0 @@ -/* -* Copyright OpenSearch Contributors -* SPDX-License-Identifier: Apache-2.0 -* -* The OpenSearch Contributors require contributions made to -* this file be licensed under the Apache-2.0 license or a -* compatible open source license. -* -*/ -package org.opensearch.test.framework; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import org.opensearch.core.xcontent.ToXContentObject; -import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.test.framework.TestSecurityConfig.Role; - -import static java.util.Objects.requireNonNull; - -/** -* The class represents mapping between backend roles {@link #backendRoles} to OpenSearch role defined by field {@link #roleName}. The -* class provides convenient builder-like methods and can be serialized to JSON. Serialization to JSON is required to store the class -* in an OpenSearch index which contains Security plugin configuration. -*/ -public class RolesMapping implements ToXContentObject { - - /** - * OpenSearch role name - */ - private String roleName; - - /** - * Backend role names - */ - private List backendRoles; - private List hostIPs; - - private boolean reserved = false; - - /** - * Creates roles mapping to OpenSearch role defined by parameter role - * @param role OpenSearch role, must not be null. - */ - public RolesMapping(Role role) { - requireNonNull(role); - this.roleName = requireNonNull(role.getName()); - this.backendRoles = new ArrayList<>(); - this.hostIPs = new ArrayList<>(); - } - - /** - * Defines backend role names - * @param backendRoles backend roles names - * @return current {@link RolesMapping} instance - */ - public RolesMapping backendRoles(String... backendRoles) { - this.backendRoles.addAll(Arrays.asList(backendRoles)); - return this; - } - - /** - * Defines host IP address - * @param hostIPs host IP address - * @return current {@link RolesMapping} instance - */ - public RolesMapping hostIPs(String... hostIPs) { - this.hostIPs.addAll(Arrays.asList(hostIPs)); - return this; - } - - /** - * Determines if role is reserved - * @param reserved true for reserved roles - * @return current {@link RolesMapping} instance - */ - public RolesMapping reserved(boolean reserved) { - this.reserved = reserved; - return this; - } - - /** - * Returns OpenSearch role name - * @return role name - */ - public String getRoleName() { - return roleName; - } - - /** - * Controls serialization to JSON - * @param xContentBuilder must not be null - * @param params not used parameter, but required by the interface {@link ToXContentObject} - * @return builder form parameter xContentBuilder - * @throws IOException denotes error during serialization to JSON - */ - @Override - public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { - xContentBuilder.startObject(); - xContentBuilder.field("reserved", reserved); - xContentBuilder.field("backend_roles", backendRoles); - xContentBuilder.field("hosts", hostIPs); - xContentBuilder.endObject(); - return xContentBuilder; - } -} diff --git a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java index 7957d1cfa4..79f10a76cf 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java +++ b/src/integrationTest/java/org/opensearch/test/framework/TestSecurityConfig.java @@ -53,6 +53,7 @@ import org.opensearch.action.update.UpdateRequest; import org.opensearch.client.Client; import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.common.Strings; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; @@ -77,11 +78,15 @@ public class TestSecurityConfig { private static final Logger log = LogManager.getLogger(TestSecurityConfig.class); + public final static String REST_ADMIN_REST_API_ACCESS = "rest_admin__rest_api_access"; + private Config config = new Config(); private Map internalUsers = new LinkedHashMap<>(); private Map roles = new LinkedHashMap<>(); private AuditConfiguration auditConfiguration; - private Map rolesMapping = new LinkedHashMap<>(); + private Map rolesMapping = new LinkedHashMap<>(); + + private Map actionGroups = new LinkedHashMap<>(); private String indexName = ".opendistro_security"; @@ -139,6 +144,17 @@ public TestSecurityConfig user(User user) { return this; } + public TestSecurityConfig withRestAdminUser(final String name, final String... permissions) { + if (internalUsers.containsKey(name)) throw new RuntimeException("REST Admin " + name + " already exists"); + user(new User(name, "REST Admin with permissions: " + Arrays.toString(permissions)).reserved(true)); + final var roleName = name + "__rest_admin_role"; + roles(new Role(roleName).clusterPermissions(permissions)); + + rolesMapping.computeIfAbsent(roleName, RoleMapping::new).users(name); + rolesMapping.computeIfAbsent(REST_ADMIN_REST_API_ACCESS, RoleMapping::new).users(name); + return this; + } + public List getUsers() { return new ArrayList<>(internalUsers.values()); } @@ -154,14 +170,18 @@ public TestSecurityConfig roles(Role... roles) { return this; } + public List roles() { + return List.copyOf(roles.values()); + } + public TestSecurityConfig audit(AuditConfiguration auditConfiguration) { this.auditConfiguration = auditConfiguration; return this; } - public TestSecurityConfig rolesMapping(RolesMapping... mappings) { - for (RolesMapping mapping : mappings) { - String roleName = mapping.getRoleName(); + public TestSecurityConfig rolesMapping(RoleMapping... mappings) { + for (RoleMapping mapping : mappings) { + String roleName = mapping.name(); if (rolesMapping.containsKey(roleName)) { throw new IllegalArgumentException("Role mapping " + roleName + " already exists"); } @@ -170,6 +190,21 @@ public TestSecurityConfig rolesMapping(RolesMapping... mappings) { return this; } + public List rolesMapping() { + return List.copyOf(rolesMapping.values()); + } + + public TestSecurityConfig actionGroups(ActionGroup... groups) { + for (final var group : groups) { + this.actionGroups.put(group.name, group); + } + return this; + } + + public List actionGroups() { + return List.copyOf(actionGroups.values()); + } + public static class Config implements ToXContentObject { private boolean anonymousAuth; @@ -252,7 +287,88 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params } } - public static class User implements UserCredentialsHolder, ToXContentObject { + public static final class ActionGroup implements ToXContentObject { + + public enum Type { + + INDEX, + + CLUSTER; + + public String type() { + return name().toLowerCase(); + } + + } + + private final String name; + + private final String description; + + private final Type type; + + private final List allowedActions; + + private Boolean hidden = null; + + private Boolean reserved = null; + + public ActionGroup(String name, Type type, String... allowedActions) { + this(name, null, type, allowedActions); + } + + public ActionGroup(String name, String description, Type type, String... allowedActions) { + this.name = name; + this.description = description; + this.type = type; + this.allowedActions = Arrays.asList(allowedActions); + } + + public String name() { + return name; + } + + public ActionGroup hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + public ActionGroup reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (hidden != null) builder.field("hidden", hidden); + if (reserved != null) builder.field("reserved", reserved); + builder.field("type", type.type()); + builder.field("allowed_actions", allowedActions); + if (description != null) builder.field("description", description); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ActionGroup that = (ActionGroup) o; + return Objects.equals(name, that.name) + && Objects.equals(description, that.description) + && type == that.type + && Objects.equals(allowedActions, that.allowedActions) + && Objects.equals(hidden, that.hidden) + && Objects.equals(reserved, that.reserved); + } + + @Override + public int hashCode() { + return Objects.hash(name, description, type, allowedActions, hidden, reserved); + } + } + + public static final class User implements UserCredentialsHolder, ToXContentObject { public final static TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles( new Role("allaccess").indexPermissions("*").on("*").clusterPermissions("*") @@ -265,9 +381,20 @@ public static class User implements UserCredentialsHolder, ToXContentObject { String requestedTenant; private Map attributes = new HashMap<>(); + private Boolean hidden = null; + + private Boolean reserved = null; + + private String description; + public User(String name) { + this(name, null); + } + + public User(String name, String description) { this.name = name; this.password = "secret"; + this.description = description; } public User password(String password) { @@ -289,6 +416,16 @@ public User backendRoles(String... backendRoles) { return this; } + public User reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + + public User hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + public User attr(String key, String value) { this.attributes.put(key, value); return this; @@ -330,9 +467,33 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params xContentBuilder.field("attributes", attributes); } + if (hidden != null) xContentBuilder.field("hidden", hidden); + if (reserved != null) xContentBuilder.field("reserved", reserved); + if (!Strings.isNullOrEmpty(description)) xContentBuilder.field("description", description); xContentBuilder.endObject(); return xContentBuilder; } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(name, user.name) + && Objects.equals(password, user.password) + && Objects.equals(roles, user.roles) + && Objects.equals(backendRoles, user.backendRoles) + && Objects.equals(requestedTenant, user.requestedTenant) + && Objects.equals(attributes, user.attributes) + && Objects.equals(hidden, user.hidden) + && Objects.equals(reserved, user.reserved) + && Objects.equals(description, user.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, password, roles, backendRoles, requestedTenant, attributes, hidden, reserved, description); + } } public static class Role implements ToXContentObject { @@ -343,8 +504,19 @@ public static class Role implements ToXContentObject { private List indexPermissions = new ArrayList<>(); + private Boolean hidden; + + private Boolean reserved; + + private String description; + public Role(String name) { + this(name, null); + } + + public Role(String name, String description) { this.name = name; + this.description = description; } public Role clusterPermissions(String... clusterPermissions) { @@ -365,6 +537,16 @@ public String getName() { return name; } + public Role hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + public Role reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + public Role clone() { Role role = new Role(this.name); role.clusterPermissions.addAll(this.clusterPermissions); @@ -383,9 +565,117 @@ public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params if (!indexPermissions.isEmpty()) { xContentBuilder.field("index_permissions", indexPermissions); } + if (hidden != null) { + xContentBuilder.field("hidden", hidden); + } + if (reserved != null) { + xContentBuilder.field("reserved", reserved); + } + if (!Strings.isNullOrEmpty(description)) xContentBuilder.field("description", description); + return xContentBuilder.endObject(); + } - xContentBuilder.endObject(); - return xContentBuilder; + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Role role = (Role) o; + return Objects.equals(name, role.name) + && Objects.equals(clusterPermissions, role.clusterPermissions) + && Objects.equals(indexPermissions, role.indexPermissions) + && Objects.equals(hidden, role.hidden) + && Objects.equals(reserved, role.reserved) + && Objects.equals(description, role.description); + } + + @Override + public int hashCode() { + return Objects.hash(name, clusterPermissions, indexPermissions, hidden, reserved, description); + } + } + + public static class RoleMapping implements ToXContentObject { + + private List users = new ArrayList<>(); + private List hosts = new ArrayList<>(); + + private final String name; + + private Boolean hidden; + + private Boolean reserved; + + private final String description; + + private List backendRoles = new ArrayList<>(); + + public RoleMapping(final String name) { + this(name, null); + } + + public RoleMapping(final String name, final String description) { + this.name = name; + this.description = description; + } + + public String name() { + return name; + } + + public RoleMapping hidden(boolean hidden) { + this.hidden = hidden; + return this; + } + + public RoleMapping reserved(boolean reserved) { + this.reserved = reserved; + return this; + } + + public RoleMapping users(String... users) { + this.users.addAll(Arrays.asList(users)); + return this; + } + + public RoleMapping hosts(String... hosts) { + this.users.addAll(Arrays.asList(hosts)); + return this; + } + + public RoleMapping backendRoles(String... backendRoles) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + return this; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + if (hidden != null) builder.field("hidden", hidden); + if (reserved != null) builder.field("reserved", reserved); + if (users != null && !users.isEmpty()) builder.field("users", users); + if (hosts != null && !hosts.isEmpty()) builder.field("hosts", hosts); + if (description != null) builder.field("description", description); + builder.field("backend_roles", backendRoles); + return builder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RoleMapping that = (RoleMapping) o; + return Objects.equals(users, that.users) + && Objects.equals(hosts, that.hosts) + && Objects.equals(name, that.name) + && Objects.equals(hidden, that.hidden) + && Objects.equals(reserved, that.reserved) + && Objects.equals(description, that.description) + && Objects.equals(backendRoles, that.backendRoles); + } + + @Override + public int hashCode() { + return Objects.hash(users, hosts, name, hidden, reserved, description, backendRoles); } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/audit/AuditLogsRule.java b/src/integrationTest/java/org/opensearch/test/framework/audit/AuditLogsRule.java index 3d13d731eb..3f9a0ae466 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/audit/AuditLogsRule.java +++ b/src/integrationTest/java/org/opensearch/test/framework/audit/AuditLogsRule.java @@ -39,6 +39,7 @@ public class AuditLogsRule implements TestRule { private static final Logger log = LogManager.getLogger(AuditLogsRule.class); private List currentTestAuditMessages; + private List currentTransportTestAuditMessages; public List getCurrentTestAuditMessages() { return currentTestAuditMessages; @@ -56,6 +57,7 @@ public void waitForAuditLogs() { private void afterWaitingForAuditLogs() { if (log.isDebugEnabled()) { log.debug("Audit records captured during test:\n{}", auditMessagesToString(currentTestAuditMessages)); + log.debug("Audit transport records captured during test:\n{}", auditMessagesToString(currentTransportTestAuditMessages)); } } @@ -63,6 +65,13 @@ public void assertExactlyOne(Predicate predicate) { assertExactly(1, predicate); } + public void assertExactlyScanAll(long expectedNumberOfAuditMessages, Predicate predicate) { + List auditMessages = new ArrayList<>(currentTestAuditMessages); + auditMessages.addAll(currentTransportTestAuditMessages); + assertExactly(exactNumberOfAuditsFulfillPredicate(expectedNumberOfAuditMessages, predicate), auditMessages); + + } + public void assertAuditLogsCount(int from, int to) { int actualCount = currentTestAuditMessages.size(); String message = "Expected audit log count is between " + from + " and " + to + " but was " + actualCount; @@ -70,10 +79,10 @@ public void assertAuditLogsCount(int from, int to) { } public void assertExactly(long expectedNumberOfAuditMessages, Predicate predicate) { - assertExactly(exactNumberOfAuditsFulfillPredicate(expectedNumberOfAuditMessages, predicate)); + assertExactly(exactNumberOfAuditsFulfillPredicate(expectedNumberOfAuditMessages, predicate), currentTestAuditMessages); } - private void assertExactly(Matcher> matcher) { + private void assertExactly(Matcher> matcher, List currentTestAuditMessages) { // pollDelay - initial delay before first evaluation Awaitility.await("Await for audit logs") .atMost(3, TimeUnit.SECONDS) @@ -82,7 +91,11 @@ private void assertExactly(Matcher> matcher) { } public void assertAtLeast(long minCount, Predicate predicate) { - assertExactly(atLeastCertainNumberOfAuditsFulfillPredicate(minCount, predicate)); + assertExactly(atLeastCertainNumberOfAuditsFulfillPredicate(minCount, predicate), currentTestAuditMessages); + } + + public void assertAtLeastTransportMessages(long minCount, Predicate predicate) { + assertExactly(atLeastCertainNumberOfAuditsFulfillPredicate(minCount, predicate), currentTransportTestAuditMessages); } private static String auditMessagesToString(List audits) { @@ -122,16 +135,35 @@ private void whenTimeoutOccurs(String methodName) { private void afterTest() { TestRuleAuditLogSink.unregisterListener(); this.currentTestAuditMessages = null; + this.currentTransportTestAuditMessages = null; } private void beforeTest(String methodName) { log.info("Start collecting audit logs before test {}", methodName); this.currentTestAuditMessages = synchronizedList(new ArrayList<>()); + this.currentTransportTestAuditMessages = synchronizedList(new ArrayList<>()); TestRuleAuditLogSink.registerListener(this); } public void onAuditMessage(AuditMessage auditMessage) { - currentTestAuditMessages.add(auditMessage); - log.debug("New audit message received '{}', total number of audit messages '{}'.", auditMessage, currentTestAuditMessages.size()); + if (auditMessage.getAsMap().keySet().contains("audit_transport_headers")) { + if (log.isDebugEnabled()) { + log.debug( + "New transport audit message received '{}', total number of transport audit messages '{}'.", + auditMessage, + currentTransportTestAuditMessages.size() + ); + } + currentTransportTestAuditMessages.add(auditMessage); + } else { + if (log.isDebugEnabled()) { + log.debug( + "New audit message received '{}', total number of audit messages '{}'.", + auditMessage, + currentTestAuditMessages.size() + ); + } + currentTestAuditMessages.add(auditMessage); + } } } diff --git a/src/integrationTest/java/org/opensearch/test/framework/audit/AuditMessagePredicate.java b/src/integrationTest/java/org/opensearch/test/framework/audit/AuditMessagePredicate.java index 4935bf0387..34565e9926 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/audit/AuditMessagePredicate.java +++ b/src/integrationTest/java/org/opensearch/test/framework/audit/AuditMessagePredicate.java @@ -29,6 +29,7 @@ import static org.opensearch.security.auditlog.impl.AuditCategory.MISSING_PRIVILEGES; import static org.opensearch.security.auditlog.impl.AuditMessage.REQUEST_LAYER; import static org.opensearch.security.auditlog.impl.AuditMessage.RESOLVED_INDICES; +import static org.opensearch.security.auditlog.impl.AuditMessage.REST_REQUEST_PARAMS; import static org.opensearch.security.auditlog.impl.AuditMessage.REST_REQUEST_PATH; public class AuditMessagePredicate implements Predicate { @@ -36,6 +37,7 @@ public class AuditMessagePredicate implements Predicate { private final AuditCategory category; private final Origin requestLayer; private final String restRequestPath; + private final Map restParams; private final String initiatingUser; private final Method requestMethod; private final String transportRequestType; @@ -47,6 +49,7 @@ private AuditMessagePredicate( AuditCategory category, Origin requestLayer, String restRequestPath, + Map restParams, String initiatingUser, Method requestMethod, String transportRequestType, @@ -57,6 +60,7 @@ private AuditMessagePredicate( this.category = category; this.requestLayer = requestLayer; this.restRequestPath = restRequestPath; + this.restParams = restParams; this.initiatingUser = initiatingUser; this.requestMethod = requestMethod; this.transportRequestType = transportRequestType; @@ -66,7 +70,7 @@ private AuditMessagePredicate( } private AuditMessagePredicate(AuditCategory category) { - this(category, null, null, null, null, null, null, null, null); + this(category, null, null, null, null, null, null, null, null, null); } public static AuditMessagePredicate auditPredicate(AuditCategory category) { @@ -110,6 +114,7 @@ public AuditMessagePredicate withLayer(Origin layer) { category, layer, restRequestPath, + restParams, initiatingUser, requestMethod, transportRequestType, @@ -124,6 +129,22 @@ public AuditMessagePredicate withRequestPath(String path) { category, requestLayer, path, + restParams, + initiatingUser, + requestMethod, + transportRequestType, + effectiveUser, + index, + privilege + ); + } + + public AuditMessagePredicate withRestParams(Map params) { + return new AuditMessagePredicate( + category, + requestLayer, + restRequestPath, + params, initiatingUser, requestMethod, transportRequestType, @@ -138,6 +159,7 @@ public AuditMessagePredicate withInitiatingUser(String user) { category, requestLayer, restRequestPath, + restParams, user, requestMethod, transportRequestType, @@ -156,6 +178,7 @@ public AuditMessagePredicate withRestMethod(Method method) { category, requestLayer, restRequestPath, + restParams, initiatingUser, method, transportRequestType, @@ -170,6 +193,7 @@ public AuditMessagePredicate withTransportRequestType(String type) { category, requestLayer, restRequestPath, + restParams, initiatingUser, requestMethod, type, @@ -184,6 +208,7 @@ public AuditMessagePredicate withEffectiveUser(String user) { category, requestLayer, restRequestPath, + restParams, initiatingUser, requestMethod, transportRequestType, @@ -206,6 +231,7 @@ public AuditMessagePredicate withIndex(String indexName) { category, requestLayer, restRequestPath, + restParams, initiatingUser, requestMethod, transportRequestType, @@ -220,6 +246,7 @@ public AuditMessagePredicate withPrivilege(String privilegeAction) { category, requestLayer, restRequestPath, + restParams, initiatingUser, requestMethod, transportRequestType, @@ -235,6 +262,7 @@ public boolean test(AuditMessage auditMessage) { predicates.add(audit -> Objects.isNull(category) || category.equals(audit.getCategory())); predicates.add(audit -> Objects.isNull(requestLayer) || requestLayer.equals(audit.getAsMap().get(REQUEST_LAYER))); predicates.add(audit -> Objects.isNull(restRequestPath) || restRequestPath.equals(audit.getAsMap().get(REST_REQUEST_PATH))); + predicates.add(audit -> Objects.isNull(restParams) || restParams.equals(auditMessage.getAsMap().get(REST_REQUEST_PARAMS))); predicates.add(audit -> Objects.isNull(initiatingUser) || initiatingUser.equals(audit.getInitiatingUser())); predicates.add(audit -> Objects.isNull(requestMethod) || requestMethod.equals(audit.getRequestMethod())); predicates.add(audit -> Objects.isNull(transportRequestType) || transportRequestType.equals(audit.getRequestType())); diff --git a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java index 2dd1dd5eea..f5a936ce7b 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java +++ b/src/integrationTest/java/org/opensearch/test/framework/certificate/TestCertificates.java @@ -55,9 +55,13 @@ public class TestCertificates { private static final Logger log = LogManager.getLogger(TestCertificates.class); - public static final Integer MAX_NUMBER_OF_NODE_CERTIFICATES = 3; + public static final Integer DEFAULT_NUMBER_OF_NODE_CERTIFICATES = 3; + + public static final String CA_SUBJECT = "DC=com,DC=example,O=Example Com Inc.,OU=Example Com Inc. Root CA,CN=Example Com Inc. Root CA"; + + public static final String LDAP_SUBJECT = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com"; + public static final String NODE_SUBJECT_PATTERN = "DC=de,L=test,O=node,OU=node,CN=node-%d.example.com"; - private static final String CA_SUBJECT = "DC=com,DC=example,O=Example Com Inc.,OU=Example Com Inc. Root CA,CN=Example Com Inc. Root CA"; private static final String ADMIN_DN = "CN=kirk,OU=client,O=client,L=test,C=de"; private static final int CERTIFICATE_VALIDITY_DAYS = 365; private static final String CERTIFICATE_FILE_EXT = ".cert"; @@ -66,13 +70,18 @@ public class TestCertificates { private final CertificateData adminCertificate; private final List nodeCertificates; + private final int numberOfNodes; + private final CertificateData ldapCertificate; public TestCertificates() { + this(DEFAULT_NUMBER_OF_NODE_CERTIFICATES); + } + + public TestCertificates(final int numberOfNodes) { this.caCertificate = createCaCertificate(); - this.nodeCertificates = IntStream.range(0, MAX_NUMBER_OF_NODE_CERTIFICATES) - .mapToObj(this::createNodeCertificate) - .collect(Collectors.toList()); + this.numberOfNodes = numberOfNodes; + this.nodeCertificates = IntStream.range(0, this.numberOfNodes).mapToObj(this::createNodeCertificate).collect(Collectors.toList()); this.ldapCertificate = createLdapCertificate(); this.adminCertificate = createAdminCertificate(ADMIN_DN); log.info("Test certificates successfully generated"); @@ -109,7 +118,7 @@ public CertificateData getRootCertificateData() { /** * Certificate for Open Search node. The certificate is derived from root certificate, returned by method {@link #getRootCertificate()} - * @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES} + * @param node is a node index. It has to be less than {@link #DEFAULT_NUMBER_OF_NODE_CERTIFICATES} * @return file which contains certificate in PEM format, defined by RFC 1421 */ public File getNodeCertificate(int node) { @@ -123,18 +132,18 @@ public CertificateData getNodeCertificateData(int node) { } private void isCorrectNodeNumber(int node) { - if (node >= MAX_NUMBER_OF_NODE_CERTIFICATES) { + if (node >= numberOfNodes) { String message = String.format( "Cannot get certificate for node %d, number of created certificates for nodes is %d", node, - MAX_NUMBER_OF_NODE_CERTIFICATES + numberOfNodes ); throw new RuntimeException(message); } } private CertificateData createNodeCertificate(Integer node) { - String subject = String.format("DC=de,L=test,O=node,OU=node,CN=node-%d.example.com", node); + final var subject = String.format(NODE_SUBJECT_PATTERN, node); String domain = String.format("node-%d.example.com", node); CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS) .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH) @@ -150,8 +159,7 @@ public CertificateData issueUserCertificate(String organizationUnit, String user } private CertificateData createLdapCertificate() { - String subject = "DC=de,L=test,O=node,OU=node,CN=ldap.example.com"; - CertificateMetadata metadata = CertificateMetadata.basicMetadata(subject, CERTIFICATE_VALIDITY_DAYS) + CertificateMetadata metadata = CertificateMetadata.basicMetadata(LDAP_SUBJECT, CERTIFICATE_VALIDITY_DAYS) .withKeyUsage(false, DIGITAL_SIGNATURE, NON_REPUDIATION, KEY_ENCIPHERMENT, CLIENT_AUTH, SERVER_AUTH) .withSubjectAlternativeName(null, List.of("localhost"), "127.0.0.1"); return CertificatesIssuerFactory.rsaBaseCertificateIssuer().issueSignedCertificate(metadata, caCertificate); @@ -164,7 +172,7 @@ public CertificateData getLdapCertificateData() { /** * It returns private key associated with node certificate returned by method {@link #getNodeCertificate(int)} * - * @param node is a node index. It has to be less than {@link #MAX_NUMBER_OF_NODE_CERTIFICATES} + * @param node is a node index. It has to be less than {@link #DEFAULT_NUMBER_OF_NODE_CERTIFICATES} * @param privateKeyPassword is a password used to encode private key, can be null to retrieve unencrypted key. * @return file which contains private key encoded in PEM format, defined * by RFC 1421 diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/ClusterManager.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/ClusterManager.java index b996b3f66f..1a22d13ac7 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/ClusterManager.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/ClusterManager.java @@ -74,6 +74,15 @@ public enum ClusterManager { new NodeSettings(NodeRole.CLUSTER_MANAGER), new NodeSettings(NodeRole.DATA), new NodeSettings(NodeRole.DATA) + ), + + THREE_CLUSTER_MANAGERS_COORDINATOR( + new NodeSettings(NodeRole.CLUSTER_MANAGER), + new NodeSettings(NodeRole.CLUSTER_MANAGER), + new NodeSettings(NodeRole.CLUSTER_MANAGER), + new NodeSettings(NodeRole.DATA), + new NodeSettings(NodeRole.DATA), + new NodeSettings() ); private List nodeSettings = new LinkedList<>(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java index 64207ead5b..894bb5baa9 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalCluster.java @@ -56,7 +56,6 @@ import org.opensearch.test.framework.AuthFailureListeners; import org.opensearch.test.framework.AuthzDomain; import org.opensearch.test.framework.OnBehalfOfConfig; -import org.opensearch.test.framework.RolesMapping; import org.opensearch.test.framework.TestIndex; import org.opensearch.test.framework.TestSecurityConfig; import org.opensearch.test.framework.TestSecurityConfig.Role; @@ -133,7 +132,7 @@ public String getSnapshotDirPath() { } @Override - public void before() throws Throwable { + public void before() { if (localOpenSearchCluster == null) { for (LocalCluster dependency : clusterDependencies) { if (!dependency.isStarted()) { @@ -142,7 +141,6 @@ public void before() throws Throwable { } for (Map.Entry entry : remotes.entrySet()) { - @SuppressWarnings("resource") InetSocketAddress transportAddress = entry.getValue().localOpenSearchCluster.clusterManagerNode().getTransportAddress(); String key = "cluster.remote." + entry.getKey() + ".seeds"; String value = transportAddress.getHostString() + ":" + transportAddress.getPort(); @@ -155,12 +153,12 @@ public void before() throws Throwable { @Override protected void after() { - System.clearProperty(INIT_CONFIGURATION_DIR); close(); } @Override public void close() { + System.clearProperty(INIT_CONFIGURATION_DIR); if (localOpenSearchCluster != null && localOpenSearchCluster.isStarted()) { try { localOpenSearchCluster.destroy(); @@ -297,6 +295,16 @@ private static void triggerConfigurationReload(Client client) { } } + public void triggerConfigurationReloadForCTypes(Client client, List cTypes, boolean ignoreFailures) { + ConfigUpdateResponse configUpdateResponse = client.execute( + ConfigUpdateAction.INSTANCE, + new ConfigUpdateRequest(cTypes.stream().map(CType::toLCString).toArray(String[]::new)) + ).actionGet(); + if (!ignoreFailures && configUpdateResponse.hasFailures()) { + throw new RuntimeException("ConfigUpdateResponse produced failures: " + configUpdateResponse.failures()); + } + } + public CertificateData getAdminCertificate() { return testCertificates.getAdminCertificateData(); } @@ -432,7 +440,7 @@ public Builder roles(Role... roles) { return this; } - public Builder rolesMapping(RolesMapping... mappings) { + public Builder rolesMapping(TestSecurityConfig.RoleMapping... mappings) { testSecurityConfig.rolesMapping(mappings); return this; } @@ -500,7 +508,7 @@ public Builder defaultConfigurationInitDirectory(String defaultConfigurationInit public LocalCluster build() { try { if (testCertificates == null) { - testCertificates = new TestCertificates(); + testCertificates = new TestCertificates(clusterManager.getNodes()); } clusterName += "_" + num.incrementAndGet(); Settings settings = nodeOverrideSettingsBuilder.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 b228fed388..8a14daeb2d 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/LocalOpenSearchCluster.java @@ -105,6 +105,8 @@ public class LocalOpenSearchCluster { private File snapshotDir; + private int nodeCounter = 0; + public LocalOpenSearchCluster( String clusterName, ClusterManager clusterManager, @@ -163,7 +165,6 @@ public void start() throws Exception { this.initialClusterManagerHosts = toHostList(clusterManagerPorts); started = true; - CompletableFuture clusterManagerNodeFuture = startNodes( clusterManager.getClusterManagerNodeSettings(), clusterManagerNodeTransportPorts, @@ -195,7 +196,6 @@ public void start() throws Exception { log.info("Startup finished. Waiting for GREEN"); waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), nodes.size()); - log.info("Started: {}", this); } @@ -213,17 +213,19 @@ public void stop() { for (Node node : nodes) { stopFutures.add(node.stop(2, TimeUnit.SECONDS)); } - CompletableFuture.allOf(stopFutures.toArray(size -> new CompletableFuture[size])).join(); + CompletableFuture.allOf(stopFutures.toArray(CompletableFuture[]::new)).join(); } public void destroy() { - stop(); - nodes.clear(); - try { - FileUtils.deleteDirectory(clusterHomeDir); - } catch (IOException e) { - log.warn("Error while deleting " + clusterHomeDir, e); + stop(); + nodes.clear(); + } finally { + try { + FileUtils.deleteDirectory(clusterHomeDir); + } catch (IOException e) { + log.warn("Error while deleting " + clusterHomeDir, e); + } } } @@ -301,10 +303,10 @@ private CompletableFuture startNodes( List> futures = new ArrayList<>(); for (NodeSettings nodeSettings : nodeSettingList) { - Node node = new Node(nodeSettings, transportPortIterator.next(), httpPortIterator.next()); + Node node = new Node(nodeCounter, nodeSettings, transportPortIterator.next(), httpPortIterator.next()); futures.add(node.start()); + nodeCounter += 1; } - return CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); } @@ -384,8 +386,10 @@ public class Node implements OpenSearchClientProvider { private PluginAwareNode node; private boolean running = false; private boolean portCollision = false; + private final int nodeNumber; - Node(NodeSettings nodeSettings, int transportPort, int httpPort) { + Node(int nodeNumber, NodeSettings nodeSettings, int transportPort, int httpPort) { + this.nodeNumber = nodeNumber; this.nodeName = createNextNodeName(requireNonNull(nodeSettings, "Node settings are required.")); this.nodeSettings = nodeSettings; this.nodeHomeDir = new File(clusterHomeDir, nodeName); @@ -515,7 +519,7 @@ private Settings getOpenSearchSettings() { if (nodeSettingsSupplier != null) { // TODO node number - return Settings.builder().put(settings).put(nodeSettingsSupplier.get(0)).build(); + return Settings.builder().put(settings).put(nodeSettingsSupplier.get(nodeNumber)).build(); } return settings; } diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/SearchRequestFactory.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/SearchRequestFactory.java index b40aa9cfcb..cfd0e09088 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/SearchRequestFactory.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/SearchRequestFactory.java @@ -70,6 +70,14 @@ public static SearchRequest searchRequestWithScroll(String indexName, int pageSi return searchRequest; } + public static SearchRequest searchRequestWithSort(String indexName) { + SearchRequest searchRequest = new SearchRequest(indexName); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + searchSourceBuilder.sort(new FieldSortBuilder("_id").order(SortOrder.ASC)); + searchRequest.source(searchSourceBuilder); + return searchRequest; + } + public static SearchRequest searchAll(String... indexNames) { SearchRequest searchRequest = new SearchRequest(indexNames); SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index e38ef949cb..e7355711fc 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -31,13 +31,12 @@ import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; -import java.net.URI; -import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; @@ -61,10 +60,9 @@ import org.apache.hc.client5.http.routing.HttpRoutePlanner; import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; -import org.apache.hc.core5.http.NameValuePair; import org.apache.hc.core5.http.io.entity.StringEntity; import org.apache.hc.core5.http.message.BasicHeader; -import org.apache.hc.core5.net.URIBuilder; +import org.apache.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -77,6 +75,7 @@ import static java.util.Objects.requireNonNull; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.notNullValue; /** @@ -107,23 +106,24 @@ public TestRestClient(InetSocketAddress nodeHttpAddress, List
headers, S this.sourceInetAddress = sourceInetAddress; } - public HttpResponse get(String path, List queryParameters, Header... headers) { - try { - URI uri = new URIBuilder(getHttpServerUri()).setPath(path).addParameters(queryParameters).build(); - return executeRequest(new HttpGet(uri), headers); - } catch (URISyntaxException ex) { - throw new RuntimeException("Incorrect URI syntax", ex); - } - } - public HttpResponse get(String path, Header... headers) { - return get(path, Collections.emptyList(), headers); + return executeRequest(new HttpGet(getHttpServerUri() + "/" + path), headers); } public HttpResponse getAuthInfo(Header... headers) { return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo?pretty"), headers); } + public HttpResponse securityHealth(Header... headers) { + return executeRequest(new HttpGet(getHttpServerUri() + "/_plugins/_security/health"), headers); + } + + public HttpResponse getAuthInfo(Map urlParams, Header... headers) { + String urlParamsString = "?" + + urlParams.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).collect(Collectors.joining("&")); + return executeRequest(new HttpGet(getHttpServerUri() + "/_opendistro/_security/authinfo" + urlParamsString), headers); + } + public void confirmCorrectCredentials(String expectedUserName) { HttpResponse response = getAuthInfo(); assertThat(response, notNullValue()); @@ -187,6 +187,10 @@ public HttpResponse post(String path) { return executeRequest(uriRequest); } + public HttpResponse patch(String path, ToXContentObject body) { + return patch(path, Strings.toString(XContentType.JSON, body)); + } + public HttpResponse patch(String path, String body) { HttpPatch uriRequest = new HttpPatch(getHttpServerUri() + "/" + path); uriRequest.setEntity(new StringEntity(body)); @@ -284,7 +288,26 @@ public HttpResponse(CloseableHttpResponse inner) throws IllegalStateException, I this.header = inner.getHeaders(); this.statusCode = inner.getCode(); this.statusReason = inner.getReasonPhrase(); + inner.close(); + + if (this.body.length() != 0) { + verifyContentType(); + } + } + + private void verifyContentType() { + final String contentType = this.getHeader(HttpHeaders.CONTENT_TYPE).getValue(); + if (contentType.contains("application/json")) { + assertThat("Response body format was not json, body: " + body, body.charAt(0), equalTo('{')); + } else { + assertThat( + "Response body format was json, whereas content-type was " + contentType + ", body: " + body, + body.charAt(0), + not(equalTo('{')) + ); + } + } public String getContentType() { @@ -402,6 +425,14 @@ public T getBodyAs(Class authInfoClass) { } } + public JsonNode bodyAsJsonNode() { + try { + return DefaultObjectMapper.readTree(getBody()); + } catch (IOException e) { + throw new RuntimeException("Cannot parse response body", e); + } + } + public void assertStatusCode(int expectedHttpStatus) { String reason = format("Expected status code is '%d', but was '%d'. Response body '%s'.", expectedHttpStatus, statusCode, body); assertThat(reason, statusCode, equalTo(expectedHttpStatus)); diff --git a/src/integrationTest/java/org/opensearch/test/framework/matcher/SearchHitContainsFieldWithValueMatcher.java b/src/integrationTest/java/org/opensearch/test/framework/matcher/SearchHitContainsFieldWithValueMatcher.java index c92924ebfe..7d1a6b3251 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/matcher/SearchHitContainsFieldWithValueMatcher.java +++ b/src/integrationTest/java/org/opensearch/test/framework/matcher/SearchHitContainsFieldWithValueMatcher.java @@ -35,6 +35,7 @@ class SearchHitContainsFieldWithValueMatcher extends TypeSafeDiagnosingMatche @Override protected boolean matchesSafely(SearchResponse searchResponse, Description mismatchDescription) { + mismatchDescription.appendText("Unexpected match in SearchResponse " + searchResponse.toString() + "\n"); Long numberOfHits = readTotalHits(searchResponse); if (numberOfHits == null) { mismatchDescription.appendText("Total number of hits is unknown."); diff --git a/src/integrationTest/resources/roles.yml b/src/integrationTest/resources/roles.yml index 02de9bf3d5..2ea7548ad6 100644 --- a/src/integrationTest/resources/roles.yml +++ b/src/integrationTest/resources/roles.yml @@ -17,3 +17,21 @@ user_limited-user__limited-role: allowed_actions: - "indices:data/read/get" - "indices:data/read/search" +flow_framework_full_access: + cluster_permissions: + - 'cluster:admin/opensearch/flow_framework/*' + - 'cluster_monitor' + index_permissions: + - index_patterns: + - '*' + allowed_actions: + - 'indices:admin/aliases/get' + - 'indices:admin/mappings/get' + - 'indices_monitor' +flow_framework_read_access: + cluster_permissions: + - 'cluster:admin/opensearch/flow_framework/workflow/get' + - 'cluster:admin/opensearch/flow_framework/workflow/search' + - 'cluster:admin/opensearch/flow_framework/workflow_state/get' + - 'cluster:admin/opensearch/flow_framework/workflow_state/search' + - 'cluster:admin/opensearch/flow_framework/workflow_step/get' 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 8c6af4279b..ea0a6378d7 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 @@ -28,6 +28,7 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; +import org.opensearch.common.logging.DeprecationLogger; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.common.Strings; @@ -48,6 +49,7 @@ public abstract class AbstractHTTPJwtAuthenticator implements HTTPAuthenticator { private final static Logger log = LogManager.getLogger(AbstractHTTPJwtAuthenticator.class); + private final static DeprecationLogger deprecationLog = DeprecationLogger.getLogger(AbstractHTTPJwtAuthenticator.class); private static final String BEARER = "bearer "; private static final Pattern BASIC = Pattern.compile("^\\s*Basic\\s.*", Pattern.CASE_INSENSITIVE); @@ -75,6 +77,13 @@ public AbstractHTTPJwtAuthenticator(Settings settings, Path configPath) { requiredAudience = settings.get("required_audience"); requiredIssuer = settings.get("required_issuer"); + if (!jwtHeaderName.equals(AUTHORIZATION)) { + deprecationLog.deprecate( + "jwt_header", + "The 'jwt_header' setting will be removed in the next major version of OpenSearch. Consult https://github.com/opensearch-project/security/issues/3886 for more details." + ); + } + try { this.keyProvider = this.initKeyProvider(settings, configPath); jwtVerifier = new JwtVerifier(keyProvider, clockSkewToleranceSeconds, requiredIssuer, requiredAudience); 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 c5c3e0ddc5..a6ff27eb6b 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 @@ -15,9 +15,11 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.Collection; +import java.util.Collections; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import java.util.regex.Pattern; import org.apache.http.HttpStatus; @@ -26,6 +28,7 @@ import org.opensearch.OpenSearchSecurityException; import org.opensearch.SpecialPermission; +import org.opensearch.common.logging.DeprecationLogger; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.security.auth.HTTPAuthenticator; @@ -44,6 +47,7 @@ public class HTTPJwtAuthenticator implements HTTPAuthenticator { protected final Logger log = LogManager.getLogger(this.getClass()); + protected final DeprecationLogger deprecationLog = DeprecationLogger.getLogger(this.getClass()); private static final Pattern BASIC = Pattern.compile("^\\s*Basic\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER = "bearer "; @@ -69,6 +73,13 @@ public HTTPJwtAuthenticator(final Settings settings, final Path configPath) { requireAudience = settings.get("required_audience"); requireIssuer = settings.get("required_issuer"); + if (!jwtHeaderName.equals(AUTHORIZATION)) { + deprecationLog.deprecate( + "jwt_header", + "The 'jwt_header' setting will be removed in the next major version of OpenSearch. Consult https://github.com/opensearch-project/security/issues/3886 for more details." + ); + } + final JwtParserBuilder jwtParserBuilder = KeyUtils.createJwtParserBuilderFromSigningKey(signingKey, log); if (jwtParserBuilder == null) { jwtParser = null; @@ -185,6 +196,14 @@ public Optional reRequestAuthentication(final SecurityRequest ); } + @Override + public Set getSensitiveUrlParams() { + if (jwtUrlParameter != null) { + return Set.of(jwtUrlParameter); + } + return Collections.emptySet(); + } + @Override public String getType() { return "jwt"; 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 44bff5c73e..125bbed073 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 @@ -39,6 +39,7 @@ import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.env.Environment; import org.opensearch.security.auth.HTTPAuthenticator; @@ -284,10 +285,12 @@ public GSSCredential run() throws GSSException { public Optional reRequestAuthentication(final SecurityRequest request, AuthCredentials creds) { final Map headers = new HashMap<>(); String responseBody = ""; + String contentType = null; + SecurityResponse response; final String negotiateResponseBody = getNegotiateResponseBody(); if (negotiateResponseBody != null) { responseBody = negotiateResponseBody; - headers.putAll(SecurityResponse.CONTENT_TYPE_APP_JSON); + contentType = XContentType.JSON.mediaType(); } if (creds == null || creds.getNativeCredentials() == null) { @@ -296,7 +299,12 @@ public Optional reRequestAuthentication(final SecurityRequest headers.put("WWW-Authenticate", "Negotiate " + Base64.getEncoder().encodeToString((byte[]) creds.getNativeCredentials())); } - return Optional.of(new SecurityResponse(SC_UNAUTHORIZED, headers, responseBody)); + if (contentType != null) { + response = new SecurityResponse(SC_UNAUTHORIZED, headers, responseBody, contentType); + } else { + response = new SecurityResponse(SC_UNAUTHORIZED, headers, responseBody); + } + return Optional.of(response); } @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 41e9305ba6..6abe934925 100644 --- a/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java +++ b/src/main/java/com/amazon/dlic/auth/http/saml/AuthTokenProcessorHandler.java @@ -137,7 +137,9 @@ private AuthTokenProcessorAction.Response handleImpl( String samlResponseBase64, String samlRequestId, String acsEndpoint, - Saml2Settings saml2Settings + Saml2Settings saml2Settings, + String requestPath // the parameter will be removed in the future as soon as we will read of legacy paths aka + // /_opendistro/_security/... ) { if (token_log.isDebugEnabled()) { try { @@ -156,7 +158,7 @@ private AuthTokenProcessorAction.Response handleImpl( final SamlResponse samlResponse = new SamlResponse(saml2Settings, acsEndpoint, samlResponseBase64); if (!samlResponse.isValid(samlRequestId)) { - log.warn("Error while validating SAML response in /_opendistro/_security/api/authtoken"); + log.warn("Error while validating SAML response in {}", requestPath); return null; } @@ -178,17 +180,14 @@ private Optional handleLowLevel(RestRequest restRequest) throw if (restRequest.getMediaType() != XContentType.JSON) { throw new OpenSearchSecurityException( - "/_opendistro/_security/api/authtoken expects content with type application/json", + restRequest.path() + " expects content with type application/json", RestStatus.UNSUPPORTED_MEDIA_TYPE ); } if (restRequest.method() != Method.POST) { - throw new OpenSearchSecurityException( - "/_opendistro/_security/api/authtoken expects POST requests", - RestStatus.METHOD_NOT_ALLOWED - ); + throw new OpenSearchSecurityException(restRequest.path() + " expects POST requests", RestStatus.METHOD_NOT_ALLOWED); } Saml2Settings saml2Settings = this.saml2SettingsProvider.getCached(); @@ -218,7 +217,13 @@ private Optional handleLowLevel(RestRequest restRequest) throw acsEndpoint = getAbsoluteAcsEndpoint(((ObjectNode) jsonRoot).get("acsEndpoint").textValue()); } - AuthTokenProcessorAction.Response responseBody = this.handleImpl(samlResponseBase64, samlRequestId, acsEndpoint, saml2Settings); + AuthTokenProcessorAction.Response responseBody = this.handleImpl( + samlResponseBase64, + samlRequestId, + acsEndpoint, + saml2Settings, + restRequest.path() + ); if (responseBody == null) { return Optional.empty(); @@ -226,10 +231,10 @@ private Optional handleLowLevel(RestRequest restRequest) throw String responseBodyString = DefaultObjectMapper.objectMapper.writeValueAsString(responseBody); - return Optional.of(new SecurityResponse(HttpStatus.SC_OK, SecurityResponse.CONTENT_TYPE_APP_JSON, responseBodyString)); + return Optional.of(new SecurityResponse(HttpStatus.SC_OK, null, responseBodyString, XContentType.JSON.mediaType())); } catch (JsonProcessingException e) { - log.warn("Error while parsing JSON for /_opendistro/_security/api/authtoken", e); - return Optional.of(new SecurityResponse(HttpStatus.SC_BAD_REQUEST, new Exception("JSON could not be parsed"))); + log.warn("Error while parsing JSON for {}", restRequest.path(), e); + return Optional.of(new SecurityResponse(HttpStatus.SC_BAD_REQUEST, "JSON could not be parsed")); } } diff --git a/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthorizationBackend.java b/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthorizationBackend.java index d8b33b2a7e..0ad0da54c6 100755 --- a/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthorizationBackend.java +++ b/src/main/java/com/amazon/dlic/auth/ldap/backend/LDAPAuthorizationBackend.java @@ -101,6 +101,7 @@ public class LDAPAuthorizationBackend implements AuthorizationBackend { protected static final Logger log = LogManager.getLogger(LDAPAuthorizationBackend.class); private final Settings settings; private final WildcardMatcher skipUsersMatcher; + private final WildcardMatcher excludeRolesMatcher; private final WildcardMatcher nestedRoleMatcher; private final Path configPath; private final List> roleBaseSettings; @@ -112,6 +113,7 @@ public class LDAPAuthorizationBackend implements AuthorizationBackend { public LDAPAuthorizationBackend(final Settings settings, final Path configPath) { this.settings = settings; this.skipUsersMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.LDAP_AUTHZ_SKIP_USERS)); + this.excludeRolesMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.LDAP_AUTHZ_EXCLUDE_ROLES)); this.nestedRoleMatcher = settings.getAsBoolean(ConfigConstants.LDAP_AUTHZ_RESOLVE_NESTED_ROLES, false) ? WildcardMatcher.from(settings.getAsList(ConfigConstants.LDAP_AUTHZ_NESTEDROLEFILTER)) : null; @@ -962,10 +964,12 @@ public void fillRoles(final User user, final AuthCredentials optionalAuthCreds) for (final LdapName roleLdapName : nestedReturn) { final String role = getRoleFromEntry(connection, roleLdapName, roleName); - if (!Strings.isNullOrEmpty(role)) { - user.addRole(role); + if (excludeRolesMatcher.test(role)) { + if (isDebugEnabled) { + log.debug("Role was excluded or empty, attribute: '{}' for entry: {}", roleName, roleLdapName); + } } else { - log.warn("No or empty attribute '{}' for entry {}", roleName, roleLdapName); + user.addRole(role); } } @@ -974,10 +978,12 @@ public void fillRoles(final User user, final AuthCredentials optionalAuthCreds) for (final LdapName roleLdapName : ldapRoles) { final String role = getRoleFromEntry(connection, roleLdapName, roleName); - if (!Strings.isNullOrEmpty(role)) { - user.addRole(role); + if (excludeRolesMatcher.test(role)) { + if (isDebugEnabled) { + log.debug("Role was excluded or empty, attribute: '{}' for entry: {}", roleName, roleLdapName); + } } else { - log.warn("No or empty attribute '{}' for entry {}", roleName, roleLdapName); + user.addRole(role); } } diff --git a/src/main/java/com/amazon/dlic/auth/ldap/util/ConfigConstants.java b/src/main/java/com/amazon/dlic/auth/ldap/util/ConfigConstants.java index 4854f80332..d3c0b798da 100755 --- a/src/main/java/com/amazon/dlic/auth/ldap/util/ConfigConstants.java +++ b/src/main/java/com/amazon/dlic/auth/ldap/util/ConfigConstants.java @@ -29,6 +29,7 @@ public final class ConfigConstants { public static final String LDAP_AUTHZ_USERROLEATTRIBUTE = "userroleattribute";// multi-value public static final String LDAP_AUTHZ_USERROLENAME = "userrolename";// multi-value public static final String LDAP_AUTHZ_SKIP_USERS = "skip_users"; + public static final String LDAP_AUTHZ_EXCLUDE_ROLES = "exclude_roles"; public static final String LDAP_AUTHZ_ROLESEARCH_ENABLED = "rolesearch_enabled"; public static final String LDAP_AUTHZ_NESTEDROLEFILTER = "nested_role_filter"; public static final String LDAP_AUTHZ_MAX_NESTED_DEPTH = "max_nested_depth"; diff --git a/src/main/java/com/amazon/dlic/auth/ldap/util/LdapHelper.java b/src/main/java/com/amazon/dlic/auth/ldap/util/LdapHelper.java index f2dffa62fd..bdb2e00754 100644 --- a/src/main/java/com/amazon/dlic/auth/ldap/util/LdapHelper.java +++ b/src/main/java/com/amazon/dlic/auth/ldap/util/LdapHelper.java @@ -122,7 +122,6 @@ private static Object escapeForwardSlash(Object input) { } else { return input; } - } } diff --git a/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthorizationBackend2.java b/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthorizationBackend2.java index e05b2e1e64..8c1569bfb6 100755 --- a/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthorizationBackend2.java +++ b/src/main/java/com/amazon/dlic/auth/ldap2/LDAPAuthorizationBackend2.java @@ -69,6 +69,7 @@ public class LDAPAuthorizationBackend2 implements AuthorizationBackend, Destroya protected static final Logger log = LogManager.getLogger(LDAPAuthorizationBackend2.class); private final Settings settings; private final WildcardMatcher skipUsersMatcher; + private final WildcardMatcher excludeRolesMatcher; private final WildcardMatcher nestedRoleMatcher; private final List> roleBaseSettings; private ConnectionPool connectionPool; @@ -80,6 +81,7 @@ public class LDAPAuthorizationBackend2 implements AuthorizationBackend, Destroya public LDAPAuthorizationBackend2(final Settings settings, final Path configPath) throws SSLConfigException { this.settings = settings; this.skipUsersMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.LDAP_AUTHZ_SKIP_USERS)); + this.excludeRolesMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.LDAP_AUTHZ_EXCLUDE_ROLES)); this.nestedRoleMatcher = settings.getAsBoolean(ConfigConstants.LDAP_AUTHZ_RESOLVE_NESTED_ROLES, false) ? WildcardMatcher.from(settings.getAsList(ConfigConstants.LDAP_AUTHZ_NESTEDROLEFILTER)) : null; @@ -329,8 +331,10 @@ private void fillRoles0(final User user, final AuthCredentials optionalAuthCreds for (final Iterator iterator = rolesResult.iterator(); iterator.hasNext();) { LdapEntry searchResultEntry = iterator.next(); LdapName ldapName = new LdapName(searchResultEntry.getDn()); - ldapRoles.add(ldapName); - resultRoleSearchBaseKeys.put(ldapName, roleSearchSettingsEntry); + if (!excludeRolesMatcher.test(searchResultEntry.getDn())) { + ldapRoles.add(ldapName); + resultRoleSearchBaseKeys.put(ldapName, roleSearchSettingsEntry); + } } } } @@ -376,10 +380,12 @@ private void fillRoles0(final User user, final AuthCredentials optionalAuthCreds for (final LdapName roleLdapName : nestedReturn) { final String role = getRoleFromEntry(connection, roleLdapName, roleName); - if (!Strings.isNullOrEmpty(role)) { - user.addRole(role); + if (excludeRolesMatcher.test(role)) { + if (isDebugEnabled) { + log.debug("Role was excluded or empty attribute '{}' for entry {}", roleName, roleLdapName); + } } else { - log.warn("No or empty attribute '{}' for entry {}", roleName, roleLdapName); + user.addRole(role); } } @@ -388,10 +394,12 @@ private void fillRoles0(final User user, final AuthCredentials optionalAuthCreds for (final LdapName roleLdapName : ldapRoles) { final String role = getRoleFromEntry(connection, roleLdapName, roleName); - if (!Strings.isNullOrEmpty(role)) { - user.addRole(role); + if (excludeRolesMatcher.test(role)) { + if (isDebugEnabled) { + log.debug("Role was excluded or empty attribute '{}' for entry {}", roleName, roleLdapName); + } } else { - log.warn("No or empty attribute '{}' for entry {}", roleName, roleLdapName); + user.addRole(role); } } diff --git a/src/main/java/org/opensearch/security/DefaultObjectMapper.java b/src/main/java/org/opensearch/security/DefaultObjectMapper.java index 48aa09541a..2d18667c54 100644 --- a/src/main/java/org/opensearch/security/DefaultObjectMapper.java +++ b/src/main/java/org/opensearch/security/DefaultObjectMapper.java @@ -35,6 +35,7 @@ import com.google.common.collect.ImmutableSet; import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; @@ -42,14 +43,40 @@ import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.exc.MismatchedInputException; import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import com.fasterxml.jackson.databind.type.TypeFactory; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import org.opensearch.SpecialPermission; +class ConfigMapSerializer extends StdSerializer> { + private static final Set SENSITIVE_CONFIG_KEYS = Set.of("password"); + + @SuppressWarnings("unchecked") + public ConfigMapSerializer() { + // Pass Map.class to the superclass + super((Class>) (Class) Map.class); + } + + @Override + public void serialize(Map value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + for (Map.Entry entry : value.entrySet()) { + if (SENSITIVE_CONFIG_KEYS.contains(entry.getKey())) { + gen.writeStringField(entry.getKey(), "******"); // Redact + } else { + gen.writeObjectField(entry.getKey(), entry.getValue()); + } + } + gen.writeEndObject(); + } +} + public class DefaultObjectMapper { public static final ObjectMapper objectMapper = new ObjectMapper(); public final static ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); @@ -180,6 +207,27 @@ public static String writeValueAsString(Object value, boolean omitDefaults) thro } + @SuppressWarnings("removal") + public static String writeValueAsStringAndRedactSensitive(Object value) throws JsonProcessingException { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + SimpleModule module = new SimpleModule(); + module.addSerializer(new ConfigMapSerializer()); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> mapper.writeValueAsString(value)); + } catch (final PrivilegedActionException e) { + throw (JsonProcessingException) e.getCause(); + } + + } + @SuppressWarnings("removal") public static T readValue(String string, TypeReference tr) throws IOException { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 3c04816c32..6e3c22e695 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -46,6 +46,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.atomic.AtomicReference; import java.util.function.BiFunction; @@ -72,6 +73,8 @@ import org.opensearch.action.search.SearchScrollAction; import org.opensearch.action.support.ActionFilter; import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.NamedDiff; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.node.DiscoveryNode; import org.opensearch.cluster.node.DiscoveryNodes; @@ -104,6 +107,7 @@ import org.opensearch.extensions.ExtensionsManager; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.identity.Subject; import org.opensearch.identity.noop.NoopSubject; import org.opensearch.index.IndexModule; @@ -114,6 +118,9 @@ import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureSettingsFactory; +import org.opensearch.plugins.SecureTransportSettingsProvider; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; import org.opensearch.rest.RestHandler; @@ -150,8 +157,7 @@ import org.opensearch.security.dlic.rest.validation.PasswordValidator; import org.opensearch.security.filter.SecurityFilter; import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.http.SecurityHttpServerTransport; -import org.opensearch.security.http.SecurityNonSslHttpServerTransport; +import org.opensearch.security.http.NonSslHttpServerTransport; import org.opensearch.security.http.XFFResolver; import org.opensearch.security.identity.SecurityTokenManager; import org.opensearch.security.privileges.PrivilegesEvaluator; @@ -167,12 +173,13 @@ import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.setting.TransportPassiveAuthSetting; +import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory; import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; -import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.state.SecurityMetadata; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.GuardedSearchOperationWrapper; import org.opensearch.security.support.HeaderHelper; @@ -199,11 +206,14 @@ import org.opensearch.transport.TransportRequestOptions; import org.opensearch.transport.TransportResponseHandler; import org.opensearch.transport.TransportService; +import org.opensearch.transport.netty4.ssl.SecureNetty4Transport; import org.opensearch.watcher.ResourceWatcherService; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; import static org.opensearch.security.support.ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION; // CS-ENFORCE-SINGLE @@ -230,7 +240,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile PrivilegesEvaluator evaluator; private volatile UserService userService; private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; - private volatile ThreadPool threadPool; private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; private volatile ClusterService cs; @@ -251,6 +260,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile OpensearchDynamicSetting transportPassiveAuthSetting; public static boolean isActionTraceEnabled() { + return actionTrace.isTraceEnabled(); } @@ -283,6 +293,10 @@ private static boolean isDisabled(final Settings settings) { return settings.getAsBoolean(ConfigConstants.SECURITY_DISABLED, false); } + private static boolean useClusterStateToInitSecurityConfig(final Settings settings) { + return settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false); + } + /** * SSL Cert Reload will be enabled only if security is not disabled and not in we are not using sslOnly mode. * @param settings Elastic configuration settings @@ -309,6 +323,20 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) return; } + if (settings.hasValue(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS)) { + verifyTLSVersion( + SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS, + settings.getAsList(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS) + ); + } + + if (settings.hasValue(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS)) { + verifyTLSVersion( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS, + settings.getAsList(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS) + ); + } + if (SSLConfig.isSslOnlyMode()) { this.sslCertReloadEnabled = false; log.warn("OpenSearch Security plugin run in ssl only mode. No authentication or authorization is performed"); @@ -338,6 +366,11 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) demoCertHashes.add("ba9c5a61065f7f6115188128ffbdaa18fca34562b78b811f082439e2bef1d282"); // esnode-key demoCertHashes.add("9948688bc4c7a198f2a0db1d91f4f54499b8626902d03361b6d43e822d3691e4"); // root-ca + // updates certs with renewed root-ca (02-2024) + demoCertHashes.add("a3556d6bb61f7bd63cb19b1c8d0078d30c12739dedb0455c5792ac8627782042"); // kirk + demoCertHashes.add("a2ce3f577a5031398c1b4f58761444d837b031d0aff7614f8b9b5e4a9d59dbd1"); // esnode + demoCertHashes.add("cd708e8dc707ae065f7ad8582979764b497f062e273d478054ab2f49c5469c6"); // root-ca + final SecurityManager sm = System.getSecurityManager(); if (sm != null) { @@ -431,6 +464,20 @@ public List run() { } } + private void verifyTLSVersion(final String settings, final List configuredProtocols) { + for (final var tls : configuredProtocols) { + if (tls.equalsIgnoreCase("TLSv1") || tls.equalsIgnoreCase("TLSv1.1")) { + deprecationLogger.deprecate( + settings, + "The '{}' setting contains {} protocol version which was deprecated since 2021 (RFC 8996). " + + "Support for it will be removed in the next major release.", + settings, + tls + ); + } + } + } + private String sha256(Path p) { if (!Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) { @@ -824,25 +871,27 @@ public void sendRequest( } @Override - public Map> getTransports( + public Map> getSecureTransports( Settings settings, ThreadPool threadPool, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService, + SecureTransportSettingsProvider secureTransportSettingsProvider, Tracer tracer ) { Map> transports = new HashMap>(); if (SSLConfig.isSslOnlyMode()) { - return super.getTransports( + return super.getSecureTransports( settings, threadPool, pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, networkService, + secureTransportSettingsProvider, tracer ); } @@ -850,18 +899,16 @@ public Map> getTransports( if (transportSSLEnabled) { transports.put( "org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport", - () -> new SecuritySSLNettyTransport( - settings, + () -> new SecureNetty4Transport( + migrateSettings(settings), Version.CURRENT, threadPool, networkService, pageCacheRecycler, namedWriteableRegistry, circuitBreakerService, - sks, - evaluateSslExceptionHandler(), sharedGroupFactory, - SSLConfig, + secureTransportSettingsProvider, tracer ) ); @@ -870,7 +917,7 @@ public Map> getTransports( } @Override - public Map> getHttpTransports( + public Map> getSecureHttpTransports( Settings settings, ThreadPool threadPool, BigArrays bigArrays, @@ -880,11 +927,12 @@ public Map> getHttpTransports( NetworkService networkService, Dispatcher dispatcher, ClusterSettings clusterSettings, + SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider, Tracer tracer ) { if (SSLConfig.isSslOnlyMode()) { - return super.getHttpTransports( + return super.getSecureHttpTransports( settings, threadPool, bigArrays, @@ -894,6 +942,7 @@ public Map> getHttpTransports( networkService, dispatcher, clusterSettings, + secureHttpTransportSettingsProvider, tracer ); } @@ -909,27 +958,25 @@ public Map> getHttpTransports( evaluateSslExceptionHandler() ); // TODO close odshst - final SecurityHttpServerTransport odshst = new SecurityHttpServerTransport( - settings, + final SecureNetty4HttpServerTransport odshst = new SecureNetty4HttpServerTransport( + migrateSettings(settings), networkService, bigArrays, threadPool, - sks, - evaluateSslExceptionHandler(), xContentRegistry, validatingDispatcher, clusterSettings, sharedGroupFactory, - tracer, - securityRestHandler + secureHttpTransportSettingsProvider, + tracer ); return Collections.singletonMap("org.opensearch.security.http.SecurityHttpServerTransport", () -> odshst); } else if (!client) { return Collections.singletonMap( "org.opensearch.security.http.SecurityHttpServerTransport", - () -> new SecurityNonSslHttpServerTransport( - settings, + () -> new NonSslHttpServerTransport( + migrateSettings(settings), networkService, bigArrays, threadPool, @@ -937,8 +984,8 @@ public Map> getHttpTransports( dispatcher, clusterSettings, sharedGroupFactory, - tracer, - securityRestHandler + secureHttpTransportSettingsProvider, + tracer ) ); } @@ -1108,7 +1155,8 @@ public Collection createComponents( cs, Objects.requireNonNull(sslExceptionHandler), Objects.requireNonNull(cih), - SSLConfig + SSLConfig, + OpenSearchSecurityPlugin::isActionTraceEnabled ); components.add(principalExtractor); @@ -1131,11 +1179,23 @@ public Collection createComponents( components.add(si); components.add(dcf); components.add(userService); - + final var allowDefaultInit = settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); + final var useClusterState = useClusterStateToInitSecurityConfig(settings); + if (!SSLConfig.isSslOnlyMode() && !isDisabled(settings) && allowDefaultInit && useClusterState) { + clusterService.addListener(cr); + } return components; } + @Override + public List getNamedWriteables() { + return List.of( + new NamedWriteableRegistry.Entry(ClusterState.Custom.class, SecurityMetadata.TYPE, SecurityMetadata::new), + new NamedWriteableRegistry.Entry(NamedDiff.class, SecurityMetadata.TYPE, SecurityMetadata::readDiffFrom) + ); + } + @Override public Settings additionalSettings() { @@ -1276,9 +1336,8 @@ public List> getSettings() { settings.add( Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false, Property.NodeScope, Property.Filtered) ); - settings.add( - Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered) - ); + settings.add(Setting.boolSetting(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered)); + settings.add(Setting.boolSetting(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false, Property.NodeScope, Property.Filtered)); settings.add( Setting.boolSetting( ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, @@ -1352,7 +1411,7 @@ public List> getSettings() { Function.identity(), Property.NodeScope ) - ); // not filtered here + ); settings.add( Setting.listSetting( ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS, @@ -1361,6 +1420,14 @@ public List> getSettings() { Property.NodeScope ) ); // not filtered here + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_AUDIT_IGNORE_HEADERS, + Collections.emptyList(), + Function.identity(), + Property.NodeScope + ) + ); settings.add( Setting.boolSetting( ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS, @@ -1393,6 +1460,7 @@ public List> getSettings() { Property.NodeScope ); case IGNORE_REQUESTS: + case IGNORE_HEADERS: return Setting.listSetting( filterEntry.getKeyWithNamespace(), Collections.emptyList(), @@ -1784,6 +1852,14 @@ public List> getSettings() { Property.Filtered ) ); + settings.add( + Setting.intSetting( + ConfigConstants.SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS, + 0, + Property.NodeScope, + Property.Filtered + ) + ); // system integration settings.add( @@ -1857,11 +1933,10 @@ public List getSettingsFilter() { @Override public void onNodeStarted(DiscoveryNode localNode) { - log.info("Node started"); - if (!SSLConfig.isSslOnlyMode() && !client && !disabled) { + this.localNode.set(localNode); + if (!SSLConfig.isSslOnlyMode() && !client && !disabled && !useClusterStateToInitSecurityConfig(settings)) { cr.initOnNodeStart(); } - this.localNode.set(localNode); final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -1953,6 +2028,11 @@ public SecurityTokenManager getTokenManager() { return tokenManager; } + @Override + public Optional getSecureSettingFactory(Settings settings) { + return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, sslExceptionHandler, securityRestHandler)); + } + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java b/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java index 64149a7c97..6f1f99a434 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java @@ -125,8 +125,10 @@ protected ConfigUpdateResponse newResponse( @Override protected ConfigUpdateNodeResponse nodeOperation(final NodeConfigUpdateRequest request) { - configurationRepository.reloadConfiguration(CType.fromStringValues((request.request.getConfigTypes()))); - backendRegistry.get().invalidateCache(); + boolean didReload = configurationRepository.reloadConfiguration(CType.fromStringValues((request.request.getConfigTypes()))); + if (didReload) { + backendRegistry.get().invalidateCache(); + } return new ConfigUpdateNodeResponse(clusterService.localNode(), request.request.getConfigTypes(), null); } diff --git a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java index 02b88bbd5c..2e88418acf 100644 --- a/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java +++ b/src/main/java/org/opensearch/security/action/onbehalf/CreateOnBehalfOfTokenAction.java @@ -32,13 +32,14 @@ import org.opensearch.security.identity.SecurityTokenManager; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_API_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class CreateOnBehalfOfTokenAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new NamedRoute.Builder().method(POST).path("/generateonbehalfoftoken").uniqueName("security:obo/create").build()), - "/_plugins/_security/api" + PLUGIN_API_ROUTE_PREFIX ); public static final long OBO_DEFAULT_EXPIRY_SECONDS = 5 * 60; diff --git a/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java b/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java index 2cffd93dfa..3b3ee742b6 100644 --- a/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java +++ b/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java @@ -12,6 +12,7 @@ package org.opensearch.security.auditlog.config; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; @@ -62,7 +63,8 @@ * "ignore_users" : [ * "kibanaserver" * ], - * "ignore_requests" : [ ] + * "ignore_requests" : [ ], + * "ignore_headers" : [ ], * }, * "compliance" : { * "enabled": true, @@ -82,6 +84,7 @@ public class AuditConfig { public static final List DEFAULT_IGNORED_USERS = Collections.singletonList("kibanaserver"); + private static Set FIELDS = DefaultObjectMapper.getFields(AuditConfig.class); private AuditConfig() { @@ -138,8 +141,14 @@ public static class Filter { private final Set ignoredAuditUsers; @JsonProperty("ignore_requests") private final Set ignoredAuditRequests; + @JsonProperty("ignore_headers") + private final Set ignoredCustomHeaders; + @JsonProperty("ignore_url_params") + private Set ignoredUrlParams; private final WildcardMatcher ignoredAuditUsersMatcher; private final WildcardMatcher ignoredAuditRequestsMatcher; + private final WildcardMatcher ignoredCustomHeadersMatcher; + private WildcardMatcher ignoredUrlParamsMatcher; private final Set disabledRestCategories; private final Set disabledTransportCategories; @@ -153,6 +162,8 @@ public static class Filter { final boolean excludeSensitiveHeaders, final Set ignoredAuditUsers, final Set ignoredAuditRequests, + final Set ignoredCustomHeaders, + final Set ignoredUrlParams, final Set disabledRestCategories, final Set disabledTransportCategories ) { @@ -166,6 +177,10 @@ public static class Filter { this.ignoredAuditUsersMatcher = WildcardMatcher.from(ignoredAuditUsers); this.ignoredAuditRequests = ignoredAuditRequests; this.ignoredAuditRequestsMatcher = WildcardMatcher.from(ignoredAuditRequests); + this.ignoredCustomHeaders = ignoredCustomHeaders; + this.ignoredCustomHeadersMatcher = WildcardMatcher.from(ignoredCustomHeaders); + this.ignoredUrlParams = ignoredUrlParams; + this.ignoredUrlParamsMatcher = WildcardMatcher.from(ignoredUrlParams); this.disabledRestCategories = disabledRestCategories; this.disabledTransportCategories = disabledTransportCategories; } @@ -183,7 +198,8 @@ public enum FilterEntries { ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES ), IGNORE_USERS("ignore_users", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS), - IGNORE_REQUESTS("ignore_requests", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS); + IGNORE_REQUESTS("ignore_requests", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS), + IGNORE_HEADERS("ignore_headers", ConfigConstants.SECURITY_AUDIT_IGNORE_HEADERS); private final String key; private final String legacyKeyWithNamespace; @@ -246,6 +262,9 @@ public static Filter from(Map properties) throws JsonProcessingE final Set ignoreAuditRequests = ImmutableSet.copyOf( getOrDefault(properties, FilterEntries.IGNORE_REQUESTS.getKey(), Collections.emptyList()) ); + final Set ignoreHeaders = ImmutableSet.copyOf( + getOrDefault(properties, FilterEntries.IGNORE_HEADERS.getKey(), Collections.emptyList()) + ); return new Filter( isRestApiAuditEnabled, @@ -256,6 +275,8 @@ public static Filter from(Map properties) throws JsonProcessingE excludeSensitiveHeaders, ignoredAuditUsers, ignoreAuditRequests, + ignoreHeaders, + new HashSet<>(), disabledRestCategories, disabledTransportCategories ); @@ -290,7 +311,7 @@ public static Filter from(Settings settings) { ); final Set ignoredAuditUsers = fromSettingStringSet(settings, FilterEntries.IGNORE_USERS, DEFAULT_IGNORED_USERS); final Set ignoreAuditRequests = fromSettingStringSet(settings, FilterEntries.IGNORE_REQUESTS, Collections.emptyList()); - + final Set ignoreHeaders = fromSettingStringSet(settings, FilterEntries.IGNORE_HEADERS, Collections.emptyList()); return new Filter( isRestApiAuditEnabled, isTransportAuditEnabled, @@ -300,6 +321,8 @@ public static Filter from(Settings settings) { excludeSensitiveHeaders, ignoredAuditUsers, ignoreAuditRequests, + ignoreHeaders, + new HashSet<>(), disabledRestCategories, disabledTransportCategories ); @@ -403,6 +426,36 @@ WildcardMatcher getIgnoredAuditRequestsMatcher() { return ignoredAuditRequestsMatcher; } + @VisibleForTesting + WildcardMatcher getIgnoredCustomHeadersMatcher() { + return ignoredCustomHeadersMatcher; + } + + @VisibleForTesting + WildcardMatcher getIgnoredUrlParamsMatcher() { + return ignoredUrlParamsMatcher; + } + + /** + * Check if the specified url param is excluded from the audit + * + * @param param + * @return true if header should be excluded + */ + public boolean shouldExcludeUrlParam(String param) { + return ignoredUrlParamsMatcher.test(param); + } + + /** + * Check if the specified header is excluded from the audit + * + * @param header + * @return true if header should be excluded + */ + public boolean shouldExcludeHeader(String header) { + return ignoredCustomHeadersMatcher.test(header); + } + /** * Check if request is excluded from audit * @param action @@ -412,6 +465,17 @@ public boolean isRequestAuditDisabled(String action) { return ignoredAuditRequestsMatcher.test(action); } + /** + * URL Params to redact for auditing + */ + public void setIgnoredUrlParams(Set ignoredUrlParams) { + if (ignoredUrlParams == null) { + return; + } + this.ignoredUrlParamsMatcher = WildcardMatcher.from(ignoredUrlParams); + this.ignoredUrlParams = ignoredUrlParams; + } + /** * Disabled categories for REST API auditing * @return set of categories @@ -440,6 +504,8 @@ public void log(Logger logger) { logger.info("Index resolution is {} during request auditing.", resolveIndices ? "enabled" : "disabled"); logger.info("Sensitive headers auditing is {}.", excludeSensitiveHeaders ? "enabled" : "disabled"); logger.info("Auditing requests from {} users is disabled.", ignoredAuditUsersMatcher); + logger.info("Auditing request headers {} is disabled.", ignoredCustomHeadersMatcher); + logger.info("Auditing request url params {} is disabled.", ignoredUrlParamsMatcher); } @Override @@ -465,6 +531,10 @@ public String toString() { + ignoredAuditUsersMatcher + ", ignoreAuditRequests=" + ignoredAuditRequestsMatcher + + ", ignoredCustomHeaders=" + + ignoredCustomHeadersMatcher + + ", ignoredUrlParamsMatcher=" + + ignoredUrlParamsMatcher + '}'; } } 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 d97adc358b..a5dd5290f6 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java @@ -19,10 +19,14 @@ import java.security.AccessController; import java.security.PrivilegedAction; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; +import java.util.SortedSet; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; @@ -62,9 +66,11 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auditlog.config.AuditConfig; +import org.opensearch.security.auth.AuthDomain; import org.opensearch.security.compliance.ComplianceConfig; import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.user.User; @@ -73,6 +79,7 @@ import org.opensearch.transport.TransportRequest; import com.flipkart.zjsonpatch.JsonDiff; +import org.greenrobot.eventbus.Subscribe; import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; @@ -88,6 +95,7 @@ public abstract class AbstractAuditLog implements AuditLog { private volatile ComplianceConfig complianceConfig; private final Environment environment; private AtomicBoolean externalConfigLogged = new AtomicBoolean(); + private final Set ignoredUrlParams = new HashSet<>(); protected abstract void enableRoutes(); @@ -120,6 +128,7 @@ protected AbstractAuditLog( } protected void onAuditConfigFilterChanged(AuditConfig.Filter auditConfigFilter) { + auditConfigFilter.setIgnoredUrlParams(ignoredUrlParams); this.auditConfigFilter = auditConfigFilter; this.auditConfigFilter.log(log); } @@ -927,12 +936,16 @@ boolean checkRestFilter(final AuditCategory category, final String effectiveUser } return false; } - - // check rest audit enabled - // check category enabled - // check action - // check ignoreAuditUsers } protected abstract void save(final AuditMessage msg); + + @Subscribe + public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { + SortedSet authDomains = Collections.unmodifiableSortedSet(dcm.getRestAuthDomains()); + ignoredUrlParams.clear(); + for (AuthDomain authDomain : authDomains) { + ignoredUrlParams.addAll(authDomain.getHttpAuthenticator().getSensitiveUrlParams()); + } + } } 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 8b24a554d1..bf3395f193 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java @@ -27,6 +27,8 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.commons.codec.digest.DigestUtils; import org.apache.hc.core5.net.URIBuilder; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.opensearch.ExceptionsHelper; import org.opensearch.cluster.service.ClusterService; @@ -59,6 +61,8 @@ public final class AuditMessage { + private static final Logger log = LogManager.getLogger(AuditMessage.class); + // clustername and cluster uuid private static final WildcardMatcher AUTHORIZATION_HEADER = WildcardMatcher.from("Authorization", false); private static final String SENSITIVE_KEY = "password"; @@ -350,18 +354,29 @@ public void addTaskParentId(String id) { } } - public void addRestParams(Map params) { + public void addRestParams(Map params, AuditConfig.Filter filter) { if (params != null && !params.isEmpty()) { - auditInfo.put(REST_REQUEST_PARAMS, new HashMap<>(params)); + Map redactedParams = new HashMap<>(); + for (Entry param : params.entrySet()) { + if (filter != null && filter.shouldExcludeUrlParam(param.getKey())) { + redactedParams.put(param.getKey(), "REDACTED"); + } else { + redactedParams.put(param.getKey(), param.getValue()); + } + } + auditInfo.put(REST_REQUEST_PARAMS, redactedParams); } } - public void addRestHeaders(Map> headers, boolean excludeSensitiveHeaders) { + public void addRestHeaders(Map> headers, boolean excludeSensitiveHeaders, AuditConfig.Filter filter) { if (headers != null && !headers.isEmpty()) { final Map> headersClone = new HashMap<>(headers); if (excludeSensitiveHeaders) { headersClone.keySet().removeIf(AUTHORIZATION_HEADER); } + if (filter != null) { + headersClone.entrySet().removeIf(entry -> filter.shouldExcludeHeader(entry.getKey())); + } auditInfo.put(REST_REQUEST_HEADERS, headersClone); } } @@ -376,14 +391,14 @@ void addRestRequestInfo(final SecurityRequest request, final AuditConfig.Filter if (request != null) { final String path = request.path().toString(); addPath(path); - addRestHeaders(request.getHeaders(), filter.shouldExcludeSensitiveHeaders()); - addRestParams(request.params()); + addRestHeaders(request.getHeaders(), filter.shouldExcludeSensitiveHeaders(), filter); + addRestParams(request.params(), filter); addRestMethod(request.method()); if (filter.shouldLogRequestBody()) { if (!(request instanceof OpenSearchRequest)) { - // The request body is only avaliable on some request sources + // The request body is only available on some request sources return; } @@ -406,8 +421,9 @@ void addRestRequestInfo(final SecurityRequest request, final AuditConfig.Filter } else { auditInfo.put(REQUEST_BODY, requestBody); } - } catch (IOException e) { + } catch (Exception e) { auditInfo.put(REQUEST_BODY, "ERROR: Unable to generate request body"); + log.error("Error while generating request body for audit log", e); } } } diff --git a/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java b/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java index f01043fa21..cf535e48b1 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java @@ -27,7 +27,7 @@ public final class Log4JSink extends AuditLogSink { public Log4JSink(final String name, final Settings settings, final String settingsPrefix, AuditLogSink fallbackSink) { super(name, settings, settingsPrefix, fallbackSink); - loggerName = settings.get(settingsPrefix + ".log4j.logger_name", "sgaudit"); + loggerName = settings.get(settingsPrefix + ".log4j.logger_name", "audit"); auditLogger = LogManager.getLogger(loggerName); logLevel = Level.toLevel(settings.get(settingsPrefix + ".log4j.level", "INFO").toUpperCase()); enabled = auditLogger.isEnabled(logLevel); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 8c8c51a745..c8eb5ae33d 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.Map; import java.util.Optional; import java.util.Set; import java.util.SortedSet; @@ -44,6 +45,7 @@ import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; import com.google.common.collect.Multimap; +import org.apache.http.HttpHeaders; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -213,7 +215,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { * @param request * @return The authenticated user, null means another roundtrip * @throws OpenSearchSecurityException - */ + */ public boolean authenticate(final SecurityRequestChannel request) { final boolean isDebugEnabled = log.isDebugEnabled(); final boolean isBlockedBasedOnAddress = request.getRemoteAddress() @@ -225,7 +227,7 @@ public boolean authenticate(final SecurityRequestChannel request) { log.debug("Rejecting REST request because of blocked address: {}", request.getRemoteAddress().orElse(null)); } - request.queueForSending(new SecurityResponse(SC_UNAUTHORIZED, new Exception("Authentication finally failed"))); + request.queueForSending(new SecurityResponse(SC_UNAUTHORIZED, "Authentication finally failed")); return false; } @@ -247,7 +249,7 @@ public boolean authenticate(final SecurityRequestChannel request) { if (!isInitialized()) { log.error("Not yet initialized (you may need to run securityadmin)"); - request.queueForSending(new SecurityResponse(SC_SERVICE_UNAVAILABLE, new Exception("OpenSearch Security not initialized."))); + request.queueForSending(new SecurityResponse(SC_SERVICE_UNAVAILABLE, "OpenSearch Security not initialized.")); return false; } @@ -309,7 +311,7 @@ public boolean authenticate(final SecurityRequestChannel request) { if (ac == null) { // no credentials found in request - if (anonymousAuthEnabled) { + if (anonymousAuthEnabled && isRequestForAnonymousLogin(request.params(), request.getHeaders())) { continue; } @@ -377,11 +379,7 @@ public boolean authenticate(final SecurityRequestChannel request) { log.error("Cannot authenticate rest user because admin user is not permitted to login via HTTP"); auditLog.logFailedLogin(authenticatedUser.getName(), true, null, request); request.queueForSending( - new SecurityResponse( - SC_FORBIDDEN, - null, - "Cannot authenticate user because admin user is not permitted to login via HTTP" - ) + new SecurityResponse(SC_FORBIDDEN, "Cannot authenticate user because admin user is not permitted to login via HTTP") ); return false; } @@ -413,19 +411,6 @@ public boolean authenticate(final SecurityRequestChannel request) { log.debug("User still not authenticated after checking {} auth domains", restAuthDomains.size()); } - if (authCredentials == null && anonymousAuthEnabled) { - final String tenant = resolveTenantFrom(request); - User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); - anonymousUser.setRequestedTenant(tenant); - - 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; - } - Optional challengeResponse = Optional.empty(); if (firstChallengingHttpAuthenticator != null) { @@ -442,6 +427,19 @@ public boolean authenticate(final SecurityRequestChannel request) { } } + if (authCredentials == null && anonymousAuthEnabled && isRequestForAnonymousLogin(request.params(), request.getHeaders())) { + final String tenant = resolveTenantFrom(request); + User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); + anonymousUser.setRequestedTenant(tenant); + + 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; + } + log.warn( "Authentication finally failed for {} from {}", authCredentials == null ? null : authCredentials.getUsername(), @@ -452,13 +450,28 @@ public boolean authenticate(final SecurityRequestChannel request) { notifyIpAuthFailureListeners(request, authCredentials); request.queueForSending( - challengeResponse.orElseGet(() -> new SecurityResponse(SC_UNAUTHORIZED, null, "Authentication finally failed")) + challengeResponse.orElseGet(() -> new SecurityResponse(SC_UNAUTHORIZED, "Authentication finally failed")) ); return false; } return authenticated; } + /** + * Checks if incoming auth request is from an anonymous user + * Defaults all requests to yes, to allow anonymous authentication to succeed + * @param params the query parameters passed in this request + * @return false only if an explicit `auth_type` param is supplied, and its value is not anonymous, OR + * if request contains no authorization headers + * otherwise returns true + */ + private boolean isRequestForAnonymousLogin(Map params, Map> headers) { + if (params.containsKey("auth_type")) { + return params.get("auth_type").equals("anonymous"); + } + return !headers.containsKey(HttpHeaders.AUTHORIZATION); + } + private String resolveTenantFrom(final SecurityRequest request) { return Optional.ofNullable(request.header("securitytenant")).orElse(request.header("security_tenant")); } diff --git a/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java b/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java index c79576ef5f..927dc0e286 100644 --- a/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java +++ b/src/main/java/org/opensearch/security/auth/HTTPAuthenticator.java @@ -26,7 +26,9 @@ package org.opensearch.security.auth; +import java.util.Collections; import java.util.Optional; +import java.util.Set; import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.util.concurrent.ThreadContext; @@ -92,4 +94,14 @@ public interface HTTPAuthenticator { default boolean supportsImpersonation() { return true; } + + /** + * Returns a set of URL parameters this authenticator supports that are considered sensitive + * and should be redacted in the audit logs + * + * @return The set of URL parameters considered sensitive for this authenticator. + */ + default Set getSensitiveUrlParams() { + return Collections.emptySet(); + } } diff --git a/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java b/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java index edc5248781..b149f2604a 100644 --- a/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java +++ b/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java @@ -30,8 +30,10 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.function.Supplier; import com.google.common.annotations.VisibleForTesting; import com.google.common.cache.CacheBuilder; @@ -73,11 +75,11 @@ @JsonInclude(JsonInclude.Include.NON_NULL) public class ComplianceConfig { + public static Set FIELDS = DefaultObjectMapper.getFields(ComplianceConfig.class); private static final Logger log = LogManager.getLogger(ComplianceConfig.class); public static final ComplianceConfig DEFAULT = ComplianceConfig.from(Settings.EMPTY); private static final int CACHE_SIZE = 1000; private static final String INTERNAL_OPENSEARCH = "internal_opensearch"; - public static Set FIELDS = DefaultObjectMapper.getFields(ComplianceConfig.class); private final boolean logExternalConfig; private final boolean logInternalConfig; @@ -104,6 +106,7 @@ public class ComplianceConfig { private final DateTimeFormatter auditLogPattern; private final String auditLogIndex; private final boolean enabled; + private final Supplier dateProvider; private ComplianceConfig( final boolean enabled, @@ -118,7 +121,8 @@ private ComplianceConfig( final Set ignoredComplianceUsersForWrite, final String securityIndex, final String destinationType, - final String destinationIndex + final String destinationIndex, + final Supplier dateProvider ) { this.enabled = enabled; this.logExternalConfig = logExternalConfig; @@ -148,6 +152,11 @@ private ComplianceConfig( try { auditLogPattern = DateTimeFormat.forPattern(destinationIndex); // throws IllegalArgumentException if no pattern } catch (IllegalArgumentException e) { + log.warn( + "Unable to translate {} as a DateTimeFormat, will instead treat this as a static audit log index name. Error: {}", + destinationIndex, + e.getMessage() + ); // no pattern auditLogIndex = destinationIndex; } catch (Exception e) { @@ -163,6 +172,8 @@ public WildcardMatcher load(String index) throws Exception { return WildcardMatcher.from(getFieldsForIndex(index)); } }); + + this.dateProvider = Optional.ofNullable(dateProvider).orElse(() -> DateTime.now(DateTimeZone.UTC)); } @VisibleForTesting @@ -177,6 +188,7 @@ public ComplianceConfig( final boolean logDiffsForWrite, final List watchedWriteIndicesPatterns, final Set ignoredComplianceUsersForWrite, + final Supplier dateProvider, Settings settings ) { this( @@ -195,7 +207,8 @@ public ComplianceConfig( settings.get( ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, "'security-auditlog-'YYYY.MM.dd" - ) + ), + dateProvider ); } @@ -253,6 +266,7 @@ public static ComplianceConfig from(Map properties, @JacksonInje logDiffsForWrite, watchedWriteIndicesPatterns, ignoredComplianceUsersForWrite, + null, settings ); } @@ -263,6 +277,16 @@ public static ComplianceConfig from(Map properties, @JacksonInje * @return compliance configuration */ public static ComplianceConfig from(Settings settings) { + return ComplianceConfig.from(settings, null); + } + + /** + * Create compliance configuration from Settings defined in opensearch.yml + * @param settings settings + * @param dateProvider how the current date/time is evalated for audit logs that rollover + * @return compliance configuration + */ + public static ComplianceConfig from(Settings settings, Supplier dateProvider) { final boolean logExternalConfig = settings.getAsBoolean( ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, false @@ -326,6 +350,7 @@ public static ComplianceConfig from(Settings settings) { logDiffsForWrite, watchedWriteIndices, ignoredComplianceUsersForWrite, + dateProvider, settings ); } @@ -469,7 +494,7 @@ private String getExpandedIndexName(DateTimeFormatter indexPattern, String index if (indexPattern == null) { return index; } - return indexPattern.print(DateTime.now(DateTimeZone.UTC)); + return indexPattern.print(dateProvider.get()); } /** @@ -507,7 +532,7 @@ public boolean writeHistoryEnabledForIndex(String index) { * @return true/false */ public boolean readHistoryEnabledForIndex(String index) { - if (!this.isEnabled()) { + if (index == null || !this.isEnabled()) { return false; } // if security index (internal index) check if internal config logging is enabled @@ -529,7 +554,7 @@ public boolean readHistoryEnabledForIndex(String index) { * @return true/false */ public boolean readHistoryEnabledForField(String index, String field) { - if (!this.isEnabled()) { + if (index == null || !this.isEnabled()) { return false; } // if security index (internal index) check if internal config logging is enabled diff --git a/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java b/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java index 210a198e2e..4cce5bb61f 100644 --- a/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java +++ b/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java @@ -33,11 +33,10 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.dlic.rest.support.Utils; import org.opensearch.security.support.HeaderHelper; +import org.opensearch.security.support.JsonFlattener; import org.opensearch.security.support.SourceFieldsContext; import org.opensearch.security.support.WildcardMatcher; -import com.github.wnameless.json.flattener.JsonFlattener; - //TODO We need to deal with caching!! //Currently we disable caching (and realtime requests) when FLS or DLS is applied //Check if we can hook in into the caches diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index 81e9f47370..44ba77428f 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -28,7 +28,10 @@ import java.io.File; import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; import java.text.SimpleDateFormat; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -36,11 +39,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import java.util.function.Supplier; +import java.util.stream.Collectors; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -55,13 +63,19 @@ import org.opensearch.action.admin.cluster.health.ClusterHealthResponse; import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.ClusterStateListener; +import org.opensearch.cluster.ClusterStateUpdateTask; import org.opensearch.cluster.health.ClusterHealthStatus; import org.opensearch.cluster.metadata.IndexMetadata; import org.opensearch.cluster.metadata.MappingMetadata; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.Priority; import org.opensearch.common.settings.Settings; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; +import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.Strings; import org.opensearch.core.rest.RestStatus; import org.opensearch.core.xcontent.MediaTypeRegistry; @@ -72,12 +86,16 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.ssl.util.ExceptionUtils; +import org.opensearch.security.state.SecurityMetadata; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.ConfigHelper; +import org.opensearch.security.support.SecurityIndexHandler; import org.opensearch.security.support.SecurityUtils; import org.opensearch.threadpool.ThreadPool; -public class ConfigurationRepository { +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; + +public class ConfigurationRepository implements ClusterStateListener { private static final Logger LOGGER = LogManager.getLogger(ConfigurationRepository.class); private final String securityIndex; @@ -86,28 +104,36 @@ public class ConfigurationRepository { private final List configurationChangedListener; private final ConfigurationLoaderSecurity7 cl; private final Settings settings; + private final Path configPath; private final ClusterService clusterService; private final AuditLog auditLog; private final ThreadPool threadPool; private DynamicConfigFactory dynamicConfigFactory; - private static final int DEFAULT_CONFIG_VERSION = 2; - private final Thread bgThread; - private final AtomicBoolean installDefaultConfig = new AtomicBoolean(); + public static final int DEFAULT_CONFIG_VERSION = 2; + private final CompletableFuture initalizeConfigTask = new CompletableFuture<>(); + private final boolean acceptInvalid; - private ConfigurationRepository( - Settings settings, + private final AtomicBoolean auditHotReloadingEnabled = new AtomicBoolean(false); + + private final AtomicBoolean initializationInProcess = new AtomicBoolean(false); + + private final SecurityIndexHandler securityIndexHandler; + + // visible for testing + protected ConfigurationRepository( + final String securityIndex, + final Settings settings, final Path configPath, - ThreadPool threadPool, - Client client, - ClusterService clusterService, - AuditLog auditLog + final ThreadPool threadPool, + final Client client, + final ClusterService clusterService, + final AuditLog auditLog, + final SecurityIndexHandler securityIndexHandler ) { - this.securityIndex = settings.get( - ConfigConstants.SECURITY_CONFIG_INDEX_NAME, - ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX - ); + this.securityIndex = securityIndex; this.settings = settings; + this.configPath = configPath; this.client = client; this.threadPool = threadPool; this.clusterService = clusterService; @@ -115,150 +141,188 @@ private ConfigurationRepository( this.configurationChangedListener = new ArrayList<>(); this.acceptInvalid = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG, false); cl = new ConfigurationLoaderSecurity7(client, threadPool, settings, clusterService); - configCache = CacheBuilder.newBuilder().build(); + this.securityIndexHandler = securityIndexHandler; + } - bgThread = new Thread(() -> { - try { - LOGGER.info("Background init thread started. Install default config?: " + installDefaultConfig.get()); - // wait for the cluster here until it will finish managed node election - while (clusterService.state().blocks().hasGlobalBlockWithStatus(RestStatus.SERVICE_UNAVAILABLE)) { - LOGGER.info("Wait for cluster to be available ..."); - TimeUnit.SECONDS.sleep(1); - } + private Path resolveConfigDir() { + return Optional.ofNullable(System.getProperty("security.default_init.dir")) + .map(Path::of) + .orElseGet(() -> new Environment(settings, configPath).configDir().resolve("opensearch-security/")); + } + + @Override + public void clusterChanged(final ClusterChangedEvent event) { + final SecurityMetadata securityMetadata = event.state().custom(SecurityMetadata.TYPE); + // init and upload sec index on the manager node only as soon as + // creation of index and upload config are done a new cluster state will be created. + // in case of failures it repeats attempt after restart + if (nodeSelectedAsManager(event)) { + if (securityMetadata == null) { + initSecurityIndex(event); + } + } + // executes reload of cache on each node on the cluster, + // since sec initialization has been finished + if (securityMetadata != null) { + executeConfigurationInitialization(securityMetadata); + } + } - if (installDefaultConfig.get()) { + private boolean nodeSelectedAsManager(final ClusterChangedEvent event) { + boolean wasClusterManager = event.previousState().nodes().isLocalNodeElectedClusterManager(); + boolean isClusterManager = event.localNodeClusterManager(); + return !wasClusterManager && isClusterManager; + } - try { - String lookupDir = System.getProperty("security.default_init.dir"); - final String cd = lookupDir != null - ? (lookupDir + "/") - : new Environment(settings, configPath).configDir().toAbsolutePath().toString() + "/opensearch-security/"; - File confFile = new File(cd + "config.yml"); - if (confFile.exists()) { - final ThreadContext threadContext = threadPool.getThreadContext(); - try (StoredContext ctx = threadContext.stashContext()) { - threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); - - createSecurityIndexIfAbsent(); - waitForSecurityIndexToBeAtLeastYellow(); - - ConfigHelper.uploadFile(client, cd + "config.yml", securityIndex, CType.CONFIG, DEFAULT_CONFIG_VERSION); - ConfigHelper.uploadFile(client, cd + "roles.yml", securityIndex, CType.ROLES, DEFAULT_CONFIG_VERSION); - ConfigHelper.uploadFile( - client, - cd + "roles_mapping.yml", - securityIndex, - CType.ROLESMAPPING, - DEFAULT_CONFIG_VERSION - ); - ConfigHelper.uploadFile( - client, - cd + "internal_users.yml", - securityIndex, - CType.INTERNALUSERS, - DEFAULT_CONFIG_VERSION - ); - ConfigHelper.uploadFile( - client, - cd + "action_groups.yml", - securityIndex, - CType.ACTIONGROUPS, - DEFAULT_CONFIG_VERSION - ); - if (DEFAULT_CONFIG_VERSION == 2) { - ConfigHelper.uploadFile( - client, - cd + "tenants.yml", - securityIndex, - CType.TENANTS, - DEFAULT_CONFIG_VERSION - ); - } - final boolean populateEmptyIfFileMissing = true; - ConfigHelper.uploadFile( - client, - cd + "nodes_dn.yml", - securityIndex, - CType.NODESDN, - DEFAULT_CONFIG_VERSION, - populateEmptyIfFileMissing - ); - ConfigHelper.uploadFile( - client, - cd + "whitelist.yml", - securityIndex, - CType.WHITELIST, - DEFAULT_CONFIG_VERSION, - populateEmptyIfFileMissing - ); - ConfigHelper.uploadFile( - client, - cd + "allowlist.yml", - securityIndex, - CType.ALLOWLIST, - DEFAULT_CONFIG_VERSION, - populateEmptyIfFileMissing - ); - - // audit.yml is not packaged by default - final String auditConfigPath = cd + "audit.yml"; - if (new File(auditConfigPath).exists()) { - ConfigHelper.uploadFile(client, auditConfigPath, securityIndex, CType.AUDIT, DEFAULT_CONFIG_VERSION); - } + public String getConfigDirectory() { + String lookupDir = System.getProperty("security.default_init.dir"); + final String cd = lookupDir != null + ? (lookupDir + "/") + : new Environment(settings, configPath).configDir().toAbsolutePath().toString() + "/opensearch-security/"; + return cd; + } + + private void initalizeClusterConfiguration(final boolean installDefaultConfig) { + try { + LOGGER.info("Background init thread started. Install default config?: " + installDefaultConfig); + // wait for the cluster here until it will finish managed node election + while (clusterService.state().blocks().hasGlobalBlockWithStatus(RestStatus.SERVICE_UNAVAILABLE)) { + LOGGER.info("Wait for cluster to be available ..."); + TimeUnit.SECONDS.sleep(1); + } + + if (installDefaultConfig) { + + try { + final String cd = getConfigDirectory(); + File confFile = new File(cd + "config.yml"); + if (confFile.exists()) { + final ThreadContext threadContext = threadPool.getThreadContext(); + try (StoredContext ctx = threadContext.stashContext()) { + threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + + createSecurityIndexIfAbsent(); + waitForSecurityIndexToBeAtLeastYellow(); + + final int initializationDelaySeconds = settings.getAsInt( + ConfigConstants.SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS, + 0 + ); + if (initializationDelaySeconds > 0) { + LOGGER.error("Test setting loaded to delay initialization for {} seconds", initializationDelaySeconds); + TimeUnit.SECONDS.sleep(initializationDelaySeconds); + } + + ConfigHelper.uploadFile(client, cd + "config.yml", securityIndex, CType.CONFIG, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, cd + "roles.yml", securityIndex, CType.ROLES, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile( + client, + cd + "roles_mapping.yml", + securityIndex, + CType.ROLESMAPPING, + DEFAULT_CONFIG_VERSION + ); + ConfigHelper.uploadFile( + client, + cd + "internal_users.yml", + securityIndex, + CType.INTERNALUSERS, + DEFAULT_CONFIG_VERSION + ); + ConfigHelper.uploadFile( + client, + cd + "action_groups.yml", + securityIndex, + CType.ACTIONGROUPS, + DEFAULT_CONFIG_VERSION + ); + if (DEFAULT_CONFIG_VERSION == 2) { + ConfigHelper.uploadFile(client, cd + "tenants.yml", securityIndex, CType.TENANTS, DEFAULT_CONFIG_VERSION); + } + final boolean populateEmptyIfFileMissing = true; + ConfigHelper.uploadFile( + client, + cd + "nodes_dn.yml", + securityIndex, + CType.NODESDN, + DEFAULT_CONFIG_VERSION, + populateEmptyIfFileMissing + ); + ConfigHelper.uploadFile( + client, + cd + "whitelist.yml", + securityIndex, + CType.WHITELIST, + DEFAULT_CONFIG_VERSION, + populateEmptyIfFileMissing + ); + ConfigHelper.uploadFile( + client, + cd + "allowlist.yml", + securityIndex, + CType.ALLOWLIST, + DEFAULT_CONFIG_VERSION, + populateEmptyIfFileMissing + ); + + // audit.yml is not packaged by default + final String auditConfigPath = cd + "audit.yml"; + if (new File(auditConfigPath).exists()) { + ConfigHelper.uploadFile(client, auditConfigPath, securityIndex, CType.AUDIT, DEFAULT_CONFIG_VERSION); } - } else { - LOGGER.error("{} does not exist", confFile.getAbsolutePath()); } - } catch (Exception e) { - LOGGER.error("Cannot apply default config (this is maybe not an error!)", e); + } else { + LOGGER.error("{} does not exist", confFile.getAbsolutePath()); } + } catch (Exception e) { + LOGGER.error("Cannot apply default config (this is maybe not an error!)", e); } + } - while (!dynamicConfigFactory.isInitialized()) { + while (!dynamicConfigFactory.isInitialized()) { + try { + LOGGER.debug("Try to load config ..."); + reloadConfiguration(Arrays.asList(CType.values()), true); + break; + } catch (Exception e) { + LOGGER.debug("Unable to load configuration due to {}", String.valueOf(ExceptionUtils.getRootCause(e))); try { - LOGGER.debug("Try to load config ..."); - reloadConfiguration(Arrays.asList(CType.values())); + TimeUnit.MILLISECONDS.sleep(3000); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + LOGGER.debug("Thread was interrupted so we cancel initialization"); break; - } catch (Exception e) { - LOGGER.debug("Unable to load configuration due to {}", String.valueOf(ExceptionUtils.getRootCause(e))); - try { - Thread.sleep(3000); - } catch (InterruptedException e1) { - Thread.currentThread().interrupt(); - LOGGER.debug("Thread was interrupted so we cancel initialization"); - break; - } } } + } + setupAuditConfigurationIfAny(cl.isAuditConfigDocPresentInIndex()); + LOGGER.info("Node '{}' initialized", clusterService.localNode().getName()); - final Set deprecatedAuditKeysInSettings = AuditConfig.getDeprecatedKeys(settings); - if (!deprecatedAuditKeysInSettings.isEmpty()) { - LOGGER.warn( - "Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0", - deprecatedAuditKeysInSettings - ); - } - final boolean isAuditConfigDocPresentInIndex = cl.isAuditConfigDocPresentInIndex(); - if (isAuditConfigDocPresentInIndex) { - if (!deprecatedAuditKeysInSettings.isEmpty()) { - LOGGER.warn("Audit configuration settings found in both index and opensearch settings (deprecated)"); - } - LOGGER.info("Hot-reloading of audit configuration is enabled"); - } else { - LOGGER.info( - "Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it." - ); - auditLog.setConfig(AuditConfig.from(settings)); - } - - LOGGER.info("Node '{}' initialized", clusterService.localNode().getName()); + } catch (Exception e) { + LOGGER.error("Unexpected exception while initializing node " + e, e); + } + } - } catch (Exception e) { - LOGGER.error("Unexpected exception while initializing node " + e, e); + private void setupAuditConfigurationIfAny(final boolean auditConfigDocPresent) { + final Set deprecatedAuditKeysInSettings = AuditConfig.getDeprecatedKeys(settings); + if (!deprecatedAuditKeysInSettings.isEmpty()) { + LOGGER.warn( + "Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0", + deprecatedAuditKeysInSettings + ); + } + if (auditConfigDocPresent) { + if (!deprecatedAuditKeysInSettings.isEmpty()) { + LOGGER.warn("Audit configuration settings found in both index and opensearch settings (deprecated)"); } - }); - + LOGGER.info("Hot-reloading of audit configuration is enabled"); + } else { + LOGGER.info( + "Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it." + ); + auditLog.setConfig(AuditConfig.from(settings)); + } } private boolean createSecurityIndexIfAbsent() { @@ -293,7 +357,7 @@ private void waitForSecurityIndexToBeAtLeastYellow() { response == null ? "no response" : (response.isTimedOut() ? "timeout" : "other, maybe red cluster") ); try { - Thread.sleep(500); + TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { // ignore Thread.currentThread().interrupt(); @@ -306,32 +370,111 @@ private void waitForSecurityIndexToBeAtLeastYellow() { } } - public void initOnNodeStart() { + void initSecurityIndex(final ClusterChangedEvent event) { + if (!event.state().metadata().hasIndex(securityIndex)) { + securityIndexHandler.createIndex( + ActionListener.wrap(r -> uploadDefaultConfiguration0(), e -> LOGGER.error("Couldn't create index {}", securityIndex, e)) + ); + } else { + // in case index was created and cluster state has not been changed (e.g. restart of the node or something) + // just upload default configuration + uploadDefaultConfiguration0(); + } + } + + private void uploadDefaultConfiguration0() { + securityIndexHandler.uploadDefaultConfiguration( + resolveConfigDir(), + ActionListener.wrap( + configuration -> clusterService.submitStateUpdateTask( + "init-security-configuration", + new ClusterStateUpdateTask(Priority.IMMEDIATE) { + @Override + public ClusterState execute(ClusterState clusterState) throws Exception { + return ClusterState.builder(clusterState) + .putCustom(SecurityMetadata.TYPE, new SecurityMetadata(Instant.now(), configuration)) + .build(); + } + + @Override + public void onFailure(String s, Exception e) { + LOGGER.error(s, e); + } + } + ), + e -> LOGGER.error("Couldn't upload default configuration", e) + ) + ); + } + + Future executeConfigurationInitialization(final SecurityMetadata securityMetadata) { + if (!initalizeConfigTask.isDone()) { + if (initializationInProcess.compareAndSet(false, true)) { + return threadPool.generic().submit(() -> { + securityIndexHandler.loadConfiguration(securityMetadata.configuration(), ActionListener.wrap(cTypeConfigs -> { + notifyConfigurationListeners(cTypeConfigs); + final var auditConfigDocPresent = cTypeConfigs.containsKey(CType.AUDIT) && cTypeConfigs.get(CType.AUDIT).notEmpty(); + setupAuditConfigurationIfAny(auditConfigDocPresent); + auditHotReloadingEnabled.getAndSet(auditConfigDocPresent); + initalizeConfigTask.complete(null); + LOGGER.info( + "Security configuration initialized. Applied hashes: {}", + securityMetadata.configuration() + .stream() + .map(c -> String.format("%s:%s", c.type().toLCString(), c.hash())) + .collect(Collectors.toList()) + ); + }, e -> LOGGER.error("Couldn't reload security configuration", e))); + return null; + }); + } + } + return CompletableFuture.completedFuture(null); + } + + @Deprecated + public CompletableFuture initOnNodeStart() { + final boolean installDefaultConfig = settings.getAsBoolean(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false); + + final Supplier> startInitialization = () -> { + new Thread(() -> { + initalizeClusterConfiguration(installDefaultConfig); + initalizeConfigTask.complete(null); + }).start(); + return initalizeConfigTask.thenApply(result -> installDefaultConfig); + }; try { - if (settings.getAsBoolean(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false)) { + if (installDefaultConfig) { LOGGER.info("Will attempt to create index {} and default configs if they are absent", securityIndex); - installDefaultConfig.set(true); - bgThread.start(); + return startInitialization.get(); } else if (settings.getAsBoolean(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) { LOGGER.info( - "Will not attempt to create index {} and default configs if they are absent. Use securityadmin to initialize cluster", + "Will not attempt to create index {} and default configs if they are absent." + + " Use securityadmin to initialize cluster", securityIndex ); - bgThread.start(); + return startInitialization.get(); } else { LOGGER.info( - "Will not attempt to create index {} and default configs if they are absent. Will not perform background initialization", + "Will not attempt to create index {} and default configs if they are absent. " + + "Will not perform background initialization", securityIndex ); + initalizeConfigTask.complete(null); + return initalizeConfigTask.thenApply(result -> installDefaultConfig); } } catch (Throwable e2) { LOGGER.error("Error during node initialization: {}", e2, e2); - bgThread.start(); + return startInitialization.get(); } } public boolean isAuditHotReloadingEnabled() { - return cl.isAuditConfigDocPresentInIndex(); + if (settings.getAsBoolean(SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE, false)) { + return auditHotReloadingEnabled.get(); + } else { + return cl.isAuditConfigDocPresentInIndex(); + } } public static ConfigurationRepository create( @@ -342,15 +485,20 @@ public static ConfigurationRepository create( ClusterService clusterService, AuditLog auditLog ) { - final ConfigurationRepository repository = new ConfigurationRepository( + final var securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); + return new ConfigurationRepository( + securityIndex, settings, configPath, threadPool, client, clusterService, - auditLog + auditLog, + new SecurityIndexHandler(securityIndex, settings, client) ); - return repository; } public void setDynamicConfigFactory(DynamicConfigFactory dynamicConfigFactory) { @@ -372,16 +520,30 @@ public SecurityDynamicConfiguration getConfiguration(CType configurationType) private final Lock LOCK = new ReentrantLock(); - public void reloadConfiguration(Collection configTypes) throws ConfigUpdateAlreadyInProgressException { + public boolean reloadConfiguration(final Collection configTypes) throws ConfigUpdateAlreadyInProgressException { + return reloadConfiguration(configTypes, false); + } + + private boolean reloadConfiguration(final Collection configTypes, final boolean fromBackgroundThread) + throws ConfigUpdateAlreadyInProgressException { + if (!fromBackgroundThread && !initalizeConfigTask.isDone()) { + LOGGER.warn("Unable to reload configuration, initalization thread has not yet completed."); + return false; + } + return loadConfigurationWithLock(configTypes); + } + + private boolean loadConfigurationWithLock(Collection configTypes) { try { if (LOCK.tryLock(60, TimeUnit.SECONDS)) { try { reloadConfiguration0(configTypes, this.acceptInvalid); + return true; } finally { LOCK.unlock(); } } else { - throw new ConfigUpdateAlreadyInProgressException("A config update is already imn progress"); + throw new ConfigUpdateAlreadyInProgressException("A config update is already in progress"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -391,8 +553,12 @@ public void reloadConfiguration(Collection configTypes) throws ConfigUpda private void reloadConfiguration0(Collection configTypes, boolean acceptInvalid) { final Map> loaded = getConfigurationsFromIndex(configTypes, false, acceptInvalid); - configCache.putAll(loaded); - notifyAboutChanges(loaded); + notifyConfigurationListeners(loaded); + } + + private void notifyConfigurationListeners(final Map> configuration) { + configCache.putAll(configuration); + notifyAboutChanges(configuration); } public synchronized void subscribeOnChange(ConfigurationChangeListener listener) { @@ -488,4 +654,24 @@ private static String formatDate(long date) { public static int getDefaultConfigVersion() { return ConfigurationRepository.DEFAULT_CONFIG_VERSION; } + + private class AccessControllerWrappedThread extends Thread { + private final Thread innerThread; + + public AccessControllerWrappedThread(Thread innerThread) { + this.innerThread = innerThread; + } + + @Override + public void run() { + AccessController.doPrivileged(new PrivilegedAction() { + + @Override + public Void run() { + innerThread.run(); + return null; + } + }); + } + } } diff --git a/src/main/java/org/opensearch/security/configuration/MaskedField.java b/src/main/java/org/opensearch/security/configuration/MaskedField.java index 8cb20ccdfe..2636047568 100644 --- a/src/main/java/org/opensearch/security/configuration/MaskedField.java +++ b/src/main/java/org/opensearch/security/configuration/MaskedField.java @@ -21,9 +21,10 @@ import com.google.common.base.Splitter; import org.apache.lucene.util.BytesRef; -import org.bouncycastle.crypto.digests.Blake2bDigest; import org.bouncycastle.util.encoders.Hex; +import com.rfksystems.blake2b.Blake2b; + public class MaskedField { private final String name; @@ -164,10 +165,12 @@ private String customHash(String in) { } private byte[] blake2bHash(byte[] in) { - final Blake2bDigest hash = new Blake2bDigest(null, 32, null, defaultSalt); + // Salt is passed incorrectly but order of parameters is retained at present to ensure full backwards compatibility + // Tracking with https://github.com/opensearch-project/security/issues/4274 + final Blake2b hash = new Blake2b(null, 32, null, defaultSalt); hash.update(in, 0, in.length); final byte[] out = new byte[hash.getDigestSize()]; - hash.doFinal(out, 0); + hash.digest(out, 0); return Hex.encode(out); } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java index b2008861aa..0602cb0bed 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java @@ -41,6 +41,7 @@ import org.opensearch.index.IndexService; import org.opensearch.security.privileges.PrivilegesEvaluator; import org.opensearch.security.securityconf.ConfigModel; +import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; import org.opensearch.security.support.WildcardMatcher; @@ -64,6 +65,8 @@ public class SecurityIndexSearcherWrapper implements CheckedFunction mappedRoles = evaluator.mapRoles(user, caller); + final SecurityRoles securityRoles = evaluator.getSecurityRoles(mappedRoles); + return !securityRoles.isPermittedOnSystemIndex(index.getName()); + } return systemIndexMatcher.test(index.getName()); } 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 04148e8b99..bee0fe5579 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 @@ -35,13 +35,16 @@ import org.opensearch.client.node.NodeClient; import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.CheckedSupplier; +import org.opensearch.common.action.ActionFuture; import org.opensearch.common.util.concurrent.ThreadContext.StoredContext; -import org.opensearch.common.xcontent.XContentHelper; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.Strings; +import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentHelper; import org.opensearch.index.engine.VersionConflictEngineException; import org.opensearch.rest.BaseRestHandler; import org.opensearch.rest.BytesRestResponse; @@ -71,7 +74,7 @@ import static org.opensearch.security.dlic.rest.api.Responses.conflict; import static org.opensearch.security.dlic.rest.api.Responses.forbidden; import static org.opensearch.security.dlic.rest.api.Responses.forbiddenMessage; -import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.internalServerError; import static org.opensearch.security.dlic.rest.api.Responses.payload; import static org.opensearch.security.dlic.rest.support.Utils.withIOException; @@ -226,7 +229,7 @@ protected final ValidationResult patchEntity( ); } - protected final ValidationResult patchEntities( + protected ValidationResult patchEntities( final RestRequest request, final JsonNode patchContent, final SecurityConfiguration securityConfiguration @@ -238,7 +241,7 @@ protected final ValidationResult patchEntities( for (final var entityName : patchEntityNames(patchContent)) { final var beforePatchEntity = configurationAsJson.get(entityName); final var patchedEntity = patchedConfigurationAsJson.get(entityName); - // verify we can process exising or updated entities + // verify we can process existing or updated entities if (beforePatchEntity != null && !Objects.equals(beforePatchEntity, patchedEntity)) { final var checkEntityCanBeProcess = endpointValidator.isAllowedToChangeImmutableEntity( SecurityConfiguration.of(entityName, configuration) @@ -336,7 +339,7 @@ final void saveOrUpdateConfiguration( final SecurityDynamicConfiguration configuration, final OnSucessActionListener onSucessActionListener ) { - saveAndUpdateConfigs(securityApiDependencies.securityIndexName(), client, getConfigType(), configuration, onSucessActionListener); + saveAndUpdateConfigsAsync(securityApiDependencies, client, getConfigType(), configuration, onSucessActionListener); } protected final String nameParam(final RestRequest request) { @@ -367,12 +370,17 @@ protected final ValidationResult loadConfiguration(final ); } - protected final ValidationResult> loadConfiguration( + protected ValidationResult> loadConfiguration( final CType cType, boolean omitSensitiveData, final boolean logComplianceEvent ) { - final var configuration = load(cType, logComplianceEvent); + SecurityDynamicConfiguration configuration; + if (omitSensitiveData) { + configuration = loadAndRedact(cType, logComplianceEvent); + } else { + configuration = load(cType, logComplianceEvent); + } if (configuration.getSeqNo() < 0) { return ValidationResult.error( @@ -448,6 +456,14 @@ protected final SecurityDynamicConfiguration load(final CType config, boolean return DynamicConfigFactory.addStatics(loaded); } + protected final SecurityDynamicConfiguration loadAndRedact(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = securityApiDependencies.configurationRepository() + .getConfigurationsFromIndex(List.of(config), logComplianceEvent) + .get(config) + .deepCloneWithRedaction(); + return DynamicConfigFactory.addStatics(loaded); + } + protected boolean ensureIndexExists() { return clusterService.state().metadata().hasConcreteIndex(securityApiDependencies.securityIndexName()); } @@ -466,36 +482,51 @@ public final void onFailure(Exception e) { if (ExceptionsHelper.unwrapCause(e) instanceof VersionConflictEngineException) { conflict(channel, e.getMessage()); } else { - internalSeverError(channel, "Error " + e.getMessage()); + internalServerError(channel, "Error " + e.getMessage()); } } } - public static void saveAndUpdateConfigs( - final String indexName, + public static ActionFuture saveAndUpdateConfigs( + final SecurityApiDependencies dependencies, + final Client client, + final CType cType, + final SecurityDynamicConfiguration configuration + ) { + final var request = createIndexRequestForConfig(dependencies, cType, configuration); + return client.index(request); + } + + public static void saveAndUpdateConfigsAsync( + final SecurityApiDependencies dependencies, final Client client, final CType cType, final SecurityDynamicConfiguration configuration, final ActionListener actionListener ) { - final IndexRequest ir = new IndexRequest(indexName); - final String id = cType.toLCString(); + final var ir = createIndexRequestForConfig(dependencies, cType, configuration); + client.index(ir, new ConfigUpdatingActionListener<>(new String[] { cType.toLCString() }, client, actionListener)); + } + private static IndexRequest createIndexRequestForConfig( + final SecurityApiDependencies dependencies, + final CType cType, + final SecurityDynamicConfiguration configuration + ) { configuration.removeStatic(); - + final BytesReference content; try { - client.index( - ir.id(id) - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .setIfSeqNo(configuration.getSeqNo()) - .setIfPrimaryTerm(configuration.getPrimaryTerm()) - .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false)), - new ConfigUpdatingActionListener<>(new String[] { id }, client, actionListener) - ); - } catch (IOException e) { + content = XContentHelper.toXContent(configuration, XContentType.JSON, ToXContent.EMPTY_PARAMS, false); + } catch (final IOException e) { throw ExceptionsHelper.convertToOpenSearchException(e); } + + return new IndexRequest(dependencies.securityIndexName()).id(cType.toLCString()) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .setIfSeqNo(configuration.getSeqNo()) + .setIfPrimaryTerm(configuration.getPrimaryTerm()) + .source(cType.toLCString(), content); } protected static class ConfigUpdatingActionListener implements ActionListener { @@ -548,7 +579,7 @@ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClie // check if .opendistro_security index has been initialized if (!ensureIndexExists()) { - return channel -> internalSeverError(channel, RequestContentValidator.ValidationError.SECURITY_NOT_INITIALIZED.message()); + return channel -> internalServerError(channel, RequestContentValidator.ValidationError.SECURITY_NOT_INITIALIZED.message()); } // check if request is authorized diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java index 172d4a537b..3032054e64 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java @@ -46,16 +46,16 @@ public class ActionGroupsApiAction extends AbstractApiAction { // legacy mapping for backwards compatibility // TODO: remove in next version new Route(Method.GET, "/actiongroup/{name}"), - new Route(Method.GET, "/actiongroup/"), + new Route(Method.GET, "/actiongroup"), new Route(Method.DELETE, "/actiongroup/{name}"), new Route(Method.PUT, "/actiongroup/{name}"), // corrected mapping, introduced in OpenSearch Security new Route(Method.GET, "/actiongroups/{name}"), - new Route(Method.GET, "/actiongroups/"), + new Route(Method.GET, "/actiongroups"), new Route(Method.DELETE, "/actiongroups/{name}"), new Route(Method.PUT, "/actiongroups/{name}"), - new Route(Method.PATCH, "/actiongroups/"), + new Route(Method.PATCH, "/actiongroups"), new Route(Method.PATCH, "/actiongroups/{name}") ) 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 47bc1f184e..997bd85bdd 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 @@ -123,9 +123,9 @@ public class AuditApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( - new Route(RestRequest.Method.GET, "/audit/"), + new Route(RestRequest.Method.GET, "/audit"), new Route(RestRequest.Method.PUT, "/audit/config"), - new Route(RestRequest.Method.PATCH, "/audit/") + new Route(RestRequest.Method.PATCH, "/audit") ) ); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiAction.java new file mode 100644 index 0000000000..f295ab8c1c --- /dev/null +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiAction.java @@ -0,0 +1,400 @@ +/* + * 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 java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestRequest.Method; +import org.opensearch.security.configuration.ConfigurationRepository; +import org.opensearch.security.dlic.rest.support.Utils; +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.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.support.ConfigHelper; +import org.opensearch.threadpool.ThreadPool; + +import com.flipkart.zjsonpatch.DiffFlags; +import com.flipkart.zjsonpatch.JsonDiff; + +import static org.opensearch.security.dlic.rest.api.Responses.badRequestMessage; +import static org.opensearch.security.dlic.rest.api.Responses.response; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; +import static org.opensearch.security.dlic.rest.support.Utils.withIOException; + +public class ConfigUpgradeApiAction extends AbstractApiAction { + + private final static Logger LOGGER = LogManager.getLogger(ConfigUpgradeApiAction.class); + + private final static Set SUPPORTED_CTYPES = ImmutableSet.of(CType.ROLES); + + private final static String REQUEST_PARAM_CONFIGS_KEY = "configs"; + + private static final List routes = addRoutesPrefix( + ImmutableList.of(new Route(Method.GET, "/_upgrade_check"), new Route(Method.POST, "/_upgrade_perform")) + ); + + @Inject + public ConfigUpgradeApiAction( + final ClusterService clusterService, + final ThreadPool threadPool, + final SecurityApiDependencies securityApiDependencies + ) { + super(Endpoint.CONFIG, clusterService, threadPool, securityApiDependencies); + this.requestHandlersBuilder.configureRequestHandlers(rhb -> { + rhb.allMethodsNotImplemented().add(Method.GET, this::canUpgrade).add(Method.POST, this::performUpgrade); + }); + } + + void canUpgrade(final RestChannel channel, final RestRequest request, final Client client) throws IOException { + getAndValidateConfigurationsToUpgrade(request).map(this::configurationDifferences).valid(differencesList -> { + final var allConfigItemChanges = differencesList.stream() + .map(kvp -> new ConfigItemChanges(kvp.v1(), kvp.v2())) + .collect(Collectors.toList()); + + final var upgradeAvailable = allConfigItemChanges.stream().anyMatch(ConfigItemChanges::hasChanges); + + final ObjectNode response = JsonNodeFactory.instance.objectNode(); + response.put("status", "OK"); + response.put("upgradeAvailable", upgradeAvailable); + + if (upgradeAvailable) { + final ObjectNode differences = JsonNodeFactory.instance.objectNode(); + allConfigItemChanges.forEach(configItemChanges -> configItemChanges.addToNode(differences)); + response.set("upgradeActions", differences); + } + channel.sendResponse(new BytesRestResponse(RestStatus.OK, XContentType.JSON.mediaType(), response.toPrettyString())); + }).error((status, toXContent) -> response(channel, status, toXContent)); + } + + void performUpgrade(final RestChannel channel, final RestRequest request, final Client client) throws IOException { + getAndValidateConfigurationsToUpgrade(request).map(this::configurationDifferences) + .map(this::verifyHasDifferences) + .map(diffs -> applyDifferences(request, client, diffs)) + .valid(updatedConfigs -> { + final var response = JsonNodeFactory.instance.objectNode(); + response.put("status", "OK"); + + final var allUpdates = JsonNodeFactory.instance.objectNode(); + updatedConfigs.forEach(configItemChanges -> configItemChanges.addToNode(allUpdates)); + response.set("upgrades", allUpdates); + + channel.sendResponse(new BytesRestResponse(RestStatus.OK, XContentType.JSON.mediaType(), response.toPrettyString())); + }) + .error((status, toXContent) -> response(channel, status, toXContent)); + } + + private ValidationResult> applyDifferences( + final RestRequest request, + final Client client, + final List> differencesToUpdate + ) { + try { + final var updatedResources = new ArrayList>(); + for (final Tuple difference : differencesToUpdate) { + updatedResources.add( + loadConfiguration(difference.v1(), false, false).map( + configuration -> patchEntities(request, difference.v2(), SecurityConfiguration.of(null, configuration)).map( + patchResults -> { + final var response = saveAndUpdateConfigs( + securityApiDependencies, + client, + difference.v1(), + patchResults.configuration() + ); + return ValidationResult.success(response.actionGet()); + } + ).map(indexResponse -> { + + final var itemsGroupedByOperation = new ConfigItemChanges(difference.v1(), difference.v2()); + return ValidationResult.success(itemsGroupedByOperation); + }) + ) + ); + } + + return ValidationResult.merge(updatedResources); + } catch (final Exception ioe) { + LOGGER.debug("Error while applying differences", ioe); + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage("Error applying configuration, see the log file to troubleshoot.") + ); + } + + } + + ValidationResult>> verifyHasDifferences(List> diffs) { + if (diffs.isEmpty()) { + return ValidationResult.error(RestStatus.BAD_REQUEST, badRequestMessage("Unable to upgrade, no differences found")); + } + + for (final var diff : diffs) { + if (diff.v2().size() == 0) { + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage("Unable to upgrade, no differences found in '" + diff.v1().toLCString() + "' config") + ); + } + } + return ValidationResult.success(diffs); + } + + private ValidationResult>> configurationDifferences(final Set configurations) { + try { + final var differences = new ArrayList>>(); + for (final var configuration : configurations) { + differences.add(computeDifferenceToUpdate(configuration)); + } + return ValidationResult.merge(differences); + } catch (final UncheckedIOException ioe) { + LOGGER.error("Error while processing differences", ioe.getCause()); + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage("Error processing configuration, see the log file to troubleshoot.") + ); + } + } + + ValidationResult> computeDifferenceToUpdate(final CType configType) { + return withIOException(() -> loadConfiguration(configType, false, false).map(activeRoles -> { + final var activeRolesJson = Utils.convertJsonToJackson(activeRoles, true); + final var defaultRolesJson = loadConfigFileAsJson(configType); + final var rawDiff = JsonDiff.asJson(activeRolesJson, defaultRolesJson, EnumSet.of(DiffFlags.OMIT_VALUE_ON_REMOVE)); + return ValidationResult.success(new Tuple<>(configType, filterRemoveOperations(rawDiff))); + })); + } + + private ValidationResult> getAndValidateConfigurationsToUpgrade(final RestRequest request) { + final String[] configs = request.paramAsStringArray(REQUEST_PARAM_CONFIGS_KEY, null); + + final Set configurations; + try { + configurations = Optional.ofNullable(configs).map(CType::fromStringValues).orElse(SUPPORTED_CTYPES); + } catch (final IllegalArgumentException iae) { + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage("Found invalid configuration option, valid options are: " + CType.lcStringValues()) + ); + } + + if (!configurations.stream().allMatch(SUPPORTED_CTYPES::contains)) { + // Remove all supported configurations + configurations.removeAll(SUPPORTED_CTYPES); + return ValidationResult.error( + RestStatus.BAD_REQUEST, + badRequestMessage("Unsupported configurations for upgrade, " + configurations) + ); + } + + return ValidationResult.success(configurations); + } + + private JsonNode filterRemoveOperations(final JsonNode diff) { + final ArrayNode filteredDiff = JsonNodeFactory.instance.arrayNode(); + diff.forEach(node -> { + if (!isRemoveOperation(node)) { + filteredDiff.add(node); + return; + } else { + if (!hasRootLevelPath(node)) { + filteredDiff.add(node); + } + } + }); + return filteredDiff; + } + + private static String pathRoot(final JsonNode node) { + return node.get("path").asText().split("/")[1]; + } + + private static boolean hasRootLevelPath(final JsonNode node) { + final var jsonPath = node.get("path").asText(); + return jsonPath.charAt(0) == '/' && !jsonPath.substring(1).contains("/"); + } + + private static boolean isRemoveOperation(final JsonNode node) { + return node.get("op").asText().equals("remove"); + } + + private SecurityDynamicConfiguration loadYamlFile(final String filepath, final CType cType) throws IOException { + return ConfigHelper.fromYamlFile(filepath, cType, ConfigurationRepository.DEFAULT_CONFIG_VERSION, 0, 0); + } + + JsonNode loadConfigFileAsJson(final CType cType) throws IOException { + final var cd = securityApiDependencies.configurationRepository().getConfigDirectory(); + final var filepath = cType.configFile(Path.of(cd)).toString(); + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + final var loadedConfiguration = loadYamlFile(filepath, cType); + return Utils.convertJsonToJackson(loadedConfiguration, true); + }); + } catch (final PrivilegedActionException e) { + LOGGER.error("Error when loading configuration from file", e); + throw (IOException) e.getCause(); + } + } + + @Override + public List routes() { + return routes; + } + + @Override + protected CType getConfigType() { + throw new UnsupportedOperationException("This class supports multiple configuration types"); + } + + @Override + protected EndpointValidator createEndpointValidator() { + return new EndpointValidator() { + + @Override + public Endpoint endpoint() { + return endpoint; + } + + @Override + public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { + return securityApiDependencies.restApiAdminPrivilegesEvaluator(); + } + + @Override + public ValidationResult entityReserved(SecurityConfiguration securityConfiguration) { + // Allow modification of reserved entities + return ValidationResult.success(securityConfiguration); + } + + @Override + public ValidationResult entityHidden(SecurityConfiguration securityConfiguration) { + // Allow modification of hidden entities + return ValidationResult.success(securityConfiguration); + } + + @Override + public RequestContentValidator createRequestContentValidator(final Object... params) { + return new ConfigUpgradeContentValidator(new RequestContentValidator.ValidationContext() { + @Override + public Object[] params() { + return params; + } + + @Override + public Settings settings() { + return securityApiDependencies.settings(); + } + + @Override + public Map allowedKeys() { + return Map.of(REQUEST_PARAM_CONFIGS_KEY, DataType.ARRAY); + } + }); + } + }; + } + + /** More permissions validation that default ContentValidator */ + static class ConfigUpgradeContentValidator extends RequestContentValidator { + + protected ConfigUpgradeContentValidator(final ValidationContext validationContext) { + super(validationContext); + } + + @Override + public ValidationResult validate(final RestRequest request, final JsonNode jsonContent) throws IOException { + return validateContentSize(jsonContent); + } + } + + /** Tranforms config changes from a raw PATCH into simplier view */ + static class ConfigItemChanges { + + private final CType config; + private final Map> itemsGroupedByOperation; + + public ConfigItemChanges(final CType config, final JsonNode differences) { + this.config = config; + this.itemsGroupedByOperation = classifyChanges(differences); + } + + public boolean hasChanges() { + return !itemsGroupedByOperation.isEmpty(); + } + + /** Adds the config item changes to the json node */ + public void addToNode(final ObjectNode node) { + final var allOperations = JsonNodeFactory.instance.objectNode(); + itemsGroupedByOperation.forEach((operation, items) -> { + final var arrayNode = allOperations.putArray(operation); + items.forEach(arrayNode::add); + }); + node.set(config.toLCString(), allOperations); + } + + /** + * Classifies the changes to this config into groupings by the type of change, for + * multiple changes types on the same item they are groupped as 'modify' + */ + private static Map> classifyChanges(final JsonNode differences) { + final var items = new HashMap(); + differences.forEach(node -> { + final var item = pathRoot(node); + final var operation = node.get("op").asText(); + if (items.containsKey(item) && !items.get(item).equals(operation)) { + items.put(item, "modify"); + } else { + items.put(item, operation); + } + }); + + final var itemsGroupedByOperation = items.entrySet() + .stream() + .collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.mapping(Map.Entry::getKey, Collectors.toList()))); + return itemsGroupedByOperation; + } + } +} diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java index d6f5e24d7d..2f579ecbd9 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java @@ -28,7 +28,7 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.threadpool.ThreadPool; -import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.internalServerError; import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -73,7 +73,7 @@ private void flushCacheApiRequestHandlers(RequestHandler.RequestHandlersBuilder public void onResponse(ConfigUpdateResponse configUpdateResponse) { if (configUpdateResponse.hasFailures()) { LOGGER.error("Cannot flush cache due to", configUpdateResponse.failures().get(0)); - internalSeverError( + internalServerError( channel, "Cannot flush cache due to " + configUpdateResponse.failures().get(0).getMessage() + "." ); @@ -86,7 +86,7 @@ public void onResponse(ConfigUpdateResponse configUpdateResponse) { @Override public void onFailure(final Exception e) { LOGGER.error("Cannot flush cache due to", e); - internalSeverError(channel, "Cannot flush cache due to " + e.getMessage() + "."); + internalServerError(channel, "Cannot flush cache due to " + e.getMessage() + "."); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 70994504bf..3cbcc18bd9 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -64,18 +64,18 @@ protected void consumeParameters(final RestRequest request) { private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(Method.GET, "/user/{name}"), - new Route(Method.GET, "/user/"), + new Route(Method.GET, "/user"), new Route(Method.POST, "/user/{name}/authtoken"), new Route(Method.DELETE, "/user/{name}"), new Route(Method.PUT, "/user/{name}"), // corrected mapping, introduced in OpenSearch Security new Route(Method.GET, "/internalusers/{name}"), - new Route(Method.GET, "/internalusers/"), + new Route(Method.GET, "/internalusers"), new Route(Method.POST, "/internalusers/{name}/authtoken"), new Route(Method.DELETE, "/internalusers/{name}"), new Route(Method.PUT, "/internalusers/{name}"), - new Route(Method.PATCH, "/internalusers/"), + new Route(Method.PATCH, "/internalusers"), new Route(Method.PATCH, "/internalusers/{name}") ) ); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java index 7f1adecd3e..b66ff0d5f3 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java @@ -61,7 +61,7 @@ import org.opensearch.threadpool.ThreadPool; import static org.opensearch.security.dlic.rest.api.Responses.badRequest; -import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.internalServerError; import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; // CS-ENFORCE-SINGLE @@ -209,7 +209,7 @@ public void onResponse(CreateIndexResponse response) { } } catch (final IOException e1) { LOGGER.error("Unable to create bulk request " + e1, e1); - internalSeverError(channel, "Unable to create bulk request."); + internalServerError(channel, "Unable to create bulk request."); return; } @@ -226,7 +226,7 @@ public void onResponse(BulkResponse response) { "Unable to upload migrated configuration because of " + response.buildFailureMessage() ); - internalSeverError( + internalServerError( channel, "Unable to upload migrated configuration (bulk index failed)." ); @@ -240,7 +240,7 @@ public void onResponse(BulkResponse response) { @Override public void onFailure(Exception e) { LOGGER.error("Unable to upload migrated configuration because of " + e, e); - internalSeverError(channel, "Unable to upload migrated configuration."); + internalServerError(channel, "Unable to upload migrated configuration."); } } ) @@ -251,7 +251,7 @@ public void onFailure(Exception e) { @Override public void onFailure(Exception e) { LOGGER.error("Unable to create opendistro_security index because of " + e, e); - internalSeverError(channel, "Unable to create opendistro_security index."); + internalServerError(channel, "Unable to create opendistro_security index."); } }); @@ -263,7 +263,7 @@ public void onFailure(Exception e) { @Override public void onFailure(Exception e) { LOGGER.error("Unable to delete opendistro_security index because of " + e, e); - internalSeverError(channel, "Unable to delete opendistro_security index."); + internalServerError(channel, "Unable to delete opendistro_security index."); } }); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java index d56025aec1..2be5778956 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java @@ -18,6 +18,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.IntStream; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -33,8 +34,10 @@ import org.opensearch.security.dlic.rest.validation.RequestContentValidator; import org.opensearch.security.dlic.rest.validation.RequestContentValidator.DataType; import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.ConfigV7; +import org.opensearch.security.securityconf.impl.v7.ConfigV7.Authc; import org.opensearch.security.support.ConfigConstants; import org.opensearch.threadpool.ThreadPool; @@ -49,6 +52,7 @@ public class MultiTenancyConfigApiAction extends AbstractApiAction { public static final String DEFAULT_TENANT_JSON_PROPERTY = "default_tenant"; public static final String PRIVATE_TENANT_ENABLED_JSON_PROPERTY = "private_tenant_enabled"; public static final String MULTITENANCY_ENABLED_JSON_PROPERTY = "multitenancy_enabled"; + public static final String SIGN_IN_OPTIONS = "sign_in_options"; private static final List ROUTES = addRoutesPrefix( ImmutableList.of(new Route(GET, "/tenancy/config"), new Route(PUT, "/tenancy/config")) @@ -119,7 +123,9 @@ public Map allowedKeys() { PRIVATE_TENANT_ENABLED_JSON_PROPERTY, DataType.BOOLEAN, MULTITENANCY_ENABLED_JSON_PROPERTY, - DataType.BOOLEAN + DataType.BOOLEAN, + SIGN_IN_OPTIONS, + DataType.ARRAY ); } }); @@ -132,6 +138,7 @@ private ToXContent multitenancyContent(final ConfigV7 config) { .field(DEFAULT_TENANT_JSON_PROPERTY, config.dynamic.kibana.default_tenant) .field(PRIVATE_TENANT_ENABLED_JSON_PROPERTY, config.dynamic.kibana.private_tenant_enabled) .field(MULTITENANCY_ENABLED_JSON_PROPERTY, config.dynamic.kibana.multitenancy_enabled) + .field(SIGN_IN_OPTIONS, config.dynamic.kibana.sign_in_options) .endObject(); } @@ -177,6 +184,12 @@ private void updateAndValidatesValues(final ConfigV7 config, final JsonNode json if (Objects.nonNull(jsonContent.findValue(MULTITENANCY_ENABLED_JSON_PROPERTY))) { config.dynamic.kibana.multitenancy_enabled = jsonContent.findValue(MULTITENANCY_ENABLED_JSON_PROPERTY).asBoolean(); } + if (jsonContent.hasNonNull(SIGN_IN_OPTIONS) && jsonContent.findValue(SIGN_IN_OPTIONS).isEmpty() == false) { + JsonNode newOptions = jsonContent.findValue(SIGN_IN_OPTIONS); + List options = getNewSignInOptions(newOptions, config.dynamic.authc); + config.dynamic.kibana.sign_in_options = options; + } + final String defaultTenant = Optional.ofNullable(config.dynamic.kibana.default_tenant).map(String::toLowerCase).orElse(""); if (!config.dynamic.kibana.private_tenant_enabled && ConfigConstants.TENANCY_PRIVATE_TENANT_NAME.equals(defaultTenant)) { @@ -202,4 +215,20 @@ private void updateAndValidatesValues(final ConfigV7 config, final JsonNode json } } + private List getNewSignInOptions(JsonNode newOptions, Authc authc) { + + Set domains = authc.getDomains().keySet(); + + return IntStream.range(0, newOptions.size()).mapToObj(newOptions::get).map(JsonNode::asText).filter(option -> { + // Checking if the new sign-in options are set in backend. + if (option.equals(DashboardSignInOption.ANONYMOUS.toString()) + || domains.stream().anyMatch(domain -> domain.contains(option.toLowerCase()))) { + return true; + } else { + throw new IllegalArgumentException( + "Validation failure: " + option.toUpperCase() + " authentication provider is not available for this cluster." + ); + } + }).map(DashboardSignInOption::valueOf).collect(Collectors.toList()); + } } 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 05c533b1d9..ff44867bd2 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 @@ -62,10 +62,10 @@ public class NodesDnApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(Method.GET, "/nodesdn/{name}"), - new Route(Method.GET, "/nodesdn/"), + new Route(Method.GET, "/nodesdn"), new Route(Method.DELETE, "/nodesdn/{name}"), new Route(Method.PUT, "/nodesdn/{name}"), - new Route(Method.PATCH, "/nodesdn/"), + new Route(Method.PATCH, "/nodesdn"), new Route(Method.PATCH, "/nodesdn/{name}") ) ); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/Responses.java b/src/main/java/org/opensearch/security/dlic/rest/api/Responses.java index 4f895d1a91..f0d90af6a0 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/Responses.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/Responses.java @@ -50,7 +50,7 @@ public static void conflict(final RestChannel channel, final String message) { response(channel, RestStatus.CONFLICT, message); } - public static void internalSeverError(final RestChannel channel, final String message) { + public static void internalServerError(final RestChannel channel, final String message) { response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java index 9af04d17ec..8b25fd5702 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java @@ -45,11 +45,11 @@ public class RolesApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( - new Route(Method.GET, "/roles/"), + new Route(Method.GET, "/roles"), new Route(Method.GET, "/roles/{name}"), new Route(Method.DELETE, "/roles/{name}"), new Route(Method.PUT, "/roles/{name}"), - new Route(Method.PATCH, "/roles/"), + new Route(Method.PATCH, "/roles"), new Route(Method.PATCH, "/roles/{name}") ) ); @@ -141,12 +141,9 @@ public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { @Override public ValidationResult isAllowedToChangeImmutableEntity(SecurityConfiguration securityConfiguration) throws IOException { - return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map(ignore -> { - if (isCurrentUserAdmin()) { - return ValidationResult.success(securityConfiguration); - } - return isAllowedToChangeEntityWithRestAdminPermissions(securityConfiguration); - }); + return EndpointValidator.super.isAllowedToChangeImmutableEntity(securityConfiguration).map( + ignore -> isAllowedToChangeEntityWithRestAdminPermissions(securityConfiguration) + ); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java index 230ce0e1a1..bc2e515e09 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java @@ -38,11 +38,11 @@ public class RolesMappingApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( - new Route(Method.GET, "/rolesmapping/"), + new Route(Method.GET, "/rolesmapping"), new Route(Method.GET, "/rolesmapping/{name}"), new Route(Method.DELETE, "/rolesmapping/{name}"), new Route(Method.PUT, "/rolesmapping/{name}"), - new Route(Method.PATCH, "/rolesmapping/"), + new Route(Method.PATCH, "/rolesmapping"), new Route(Method.PATCH, "/rolesmapping/{name}") ) ); @@ -106,14 +106,11 @@ public ValidationResult isAllowedToChangeImmutableEntity( public ValidationResult isAllowedToChangeRoleMappingWithRestAdminPermissions( SecurityConfiguration securityConfiguration ) throws IOException { - return loadConfiguration(CType.ROLES, false, false).map(rolesConfiguration -> { - if (isCurrentUserAdmin()) { - return ValidationResult.success(securityConfiguration); - } - return isAllowedToChangeEntityWithRestAdminPermissions( + return loadConfiguration(CType.ROLES, false, false).map( + rolesConfiguration -> isAllowedToChangeEntityWithRestAdminPermissions( SecurityConfiguration.of(securityConfiguration.entityName(), rolesConfiguration) - ); - }).map(ignore -> ValidationResult.success(securityConfiguration)); + ) + ).map(ignore -> ValidationResult.success(securityConfiguration)); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index b0d46f8774..f38cf0580d 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,8 @@ public static Collection getHandler( new AllowlistApiAction(Endpoint.ALLOWLIST, clusterService, threadPool, securityApiDependencies), new AuditApiAction(clusterService, threadPool, securityApiDependencies), new MultiTenancyConfigApiAction(clusterService, threadPool, securityApiDependencies), - new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies) + new SecuritySSLCertsApiAction(clusterService, threadPool, securityKeyStore, certificatesReloadEnabled, securityApiDependencies), + new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java index 48e1c9b704..e60070288e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsApiAction.java @@ -50,7 +50,7 @@ */ 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/")) + ImmutableList.of(new Route(Method.GET, "/ssl/certs"), new Route(Method.PUT, "/ssl/{certType}/reloadcerts")) ); private final SecurityKeyStore securityKeyStore; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java index 28fd6dcdcb..e16d31ba6f 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java @@ -50,10 +50,10 @@ public class TenantsApiAction extends AbstractApiAction { private static final List routes = addRoutesPrefix( ImmutableList.of( new Route(Method.GET, "/tenants/{name}"), - new Route(Method.GET, "/tenants/"), + new Route(Method.GET, "/tenants"), new Route(Method.DELETE, "/tenants/{name}"), new Route(Method.PUT, "/tenants/{name}"), - new Route(Method.PATCH, "/tenants/"), + new Route(Method.PATCH, "/tenants"), new Route(Method.PATCH, "/tenants/{name}") ) ); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java index 93f1cd35c3..1d56ed80f9 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java @@ -40,7 +40,7 @@ import org.opensearch.threadpool.ThreadPool; import static org.opensearch.security.dlic.rest.api.Responses.badRequest; -import static org.opensearch.security.dlic.rest.api.Responses.internalSeverError; +import static org.opensearch.security.dlic.rest.api.Responses.internalServerError; import static org.opensearch.security.dlic.rest.api.Responses.ok; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; @@ -125,7 +125,7 @@ protected void validate(RestChannel channel, RestRequest request) throws IOExcep ok(channel, "OK."); } catch (Exception e) { - internalSeverError(channel, "Configuration is not valid."); + internalServerError(channel, "Configuration is not valid."); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 74b7cd415a..ee68a629c6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -55,9 +55,19 @@ import org.opensearch.security.user.User; import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; +import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; public class Utils { + public final static String PLUGIN_ROUTE_PREFIX = "/" + PLUGINS_PREFIX; + + public final static String LEGACY_PLUGIN_ROUTE_PREFIX = "/" + LEGACY_OPENDISTRO_PREFIX; + + public final static String PLUGIN_API_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/api"; + + public final static String LEGACY_PLUGIN_API_ROUTE_PREFIX = LEGACY_PLUGIN_ROUTE_PREFIX + "/api"; + private static final ObjectMapper internalMapper = new ObjectMapper(); public static Map convertJsonToxToStructuredMap(ToXContent jsonContent) { @@ -217,7 +227,7 @@ public static Set generateFieldResourcePaths(final Set fields, f *Total number of routes is expanded as twice as the number of routes passed in */ public static List addRoutesPrefix(List routes) { - return addRoutesPrefix(routes, "/_opendistro/_security/api", "/_plugins/_security/api"); + return addRoutesPrefix(routes, LEGACY_PLUGIN_API_ROUTE_PREFIX, PLUGIN_API_ROUTE_PREFIX); } /** @@ -248,7 +258,7 @@ public static List addRoutesPrefix(List routes, final String... pr *Total number of routes is expanded as twice as the number of routes passed in */ public static List addDeprecatedRoutesPrefix(List deprecatedRoutes) { - return addDeprecatedRoutesPrefix(deprecatedRoutes, "/_opendistro/_security/api", "/_plugins/_security/api"); + return addDeprecatedRoutesPrefix(deprecatedRoutes, LEGACY_PLUGIN_API_ROUTE_PREFIX, PLUGIN_API_ROUTE_PREFIX); } /** diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java index 5879272b30..17c8b1f2ba 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/EndpointValidator.java @@ -156,21 +156,19 @@ default ValidationResult> validateRoles( default ValidationResult isAllowedToChangeEntityWithRestAdminPermissions( final SecurityConfiguration securityConfiguration ) throws IOException { + final var configuration = securityConfiguration.configuration(); if (securityConfiguration.entityExists()) { - final var configuration = securityConfiguration.configuration(); final var existingEntity = configuration.getCEntry(securityConfiguration.entityName()); if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(existingEntity)) { - return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } - } else { - final var configuration = securityConfiguration.configuration(); - final var configEntityContent = Utils.toConfigObject( + } + if (securityConfiguration.requestContent() != null) { + final var newConfigEntityContent = Utils.toConfigObject( securityConfiguration.requestContent(), configuration.getImplementingClass() ); - if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(configEntityContent)) { - + if (restApiAdminPrivilegesEvaluator().containsRestApiAdminPermissions(newConfigEntityContent)) { return ValidationResult.error(RestStatus.FORBIDDEN, forbiddenMessage("Access denied")); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java index 452bdd72e4..4d9faf096c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/RequestContentValidator.java @@ -142,7 +142,7 @@ private ValidationResult parseRequestContent(final RestRequest request } } - private ValidationResult validateContentSize(final JsonNode jsonContent) { + protected ValidationResult validateContentSize(final JsonNode jsonContent) { if (jsonContent.isEmpty()) { this.validationError = ValidationError.PAYLOAD_MANDATORY; return ValidationResult.error(RestStatus.BAD_REQUEST, this); @@ -150,7 +150,7 @@ private ValidationResult validateContentSize(final JsonNode jsonConten return ValidationResult.success(jsonContent); } - private ValidationResult validateJsonKeys(final JsonNode jsonContent) { + protected ValidationResult validateJsonKeys(final JsonNode jsonContent) { final Set requestedKeys = new HashSet<>(); jsonContent.fieldNames().forEachRemaining(requestedKeys::add); // mandatory settings, one of ... diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java b/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java index ea782ea504..921ed4675d 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/ValidationResult.java @@ -12,7 +12,9 @@ package org.opensearch.security.dlic.rest.validation; import java.io.IOException; +import java.util.List; import java.util.Objects; +import java.util.stream.Collectors; import org.opensearch.common.CheckedBiConsumer; import org.opensearch.common.CheckedConsumer; @@ -50,6 +52,22 @@ public static ValidationResult error(final RestStatus status, final ToXCo return new ValidationResult<>(status, errorMessage); } + /** + * Transforms a list of validation results into a single validation result of that lists contents. + * If any of the validation results are not valid, the first is returned as the error. + */ + public static ValidationResult> merge(final List> results) { + if (results.stream().allMatch(ValidationResult::isValid)) { + return success(results.stream().map(result -> result.content).collect(Collectors.toList())); + } + + return results.stream() + .filter(result -> !result.isValid()) + .map(failedResult -> new ValidationResult>(failedResult.status, failedResult.errorMessage)) + .findFirst() + .get(); + } + public ValidationResult map(final CheckedFunction, IOException> mapper) throws IOException { if (content != null) { return Objects.requireNonNull(mapper).apply(content); @@ -82,5 +100,4 @@ public boolean isValid() { public ToXContent errorMessage() { return errorMessage; } - } diff --git a/src/main/java/org/opensearch/security/filter/NettyAttribute.java b/src/main/java/org/opensearch/security/filter/NettyAttribute.java index 685e94e199..3a035a390b 100644 --- a/src/main/java/org/opensearch/security/filter/NettyAttribute.java +++ b/src/main/java/org/opensearch/security/filter/NettyAttribute.java @@ -1,8 +1,18 @@ +/* + * 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.filter; import java.util.Optional; -import org.opensearch.http.netty4.Netty4HttpChannel; +import org.opensearch.http.HttpChannel; import org.opensearch.rest.RestRequest; import io.netty.channel.Channel; @@ -15,11 +25,12 @@ public class NettyAttribute { * Gets an attribute value from the request context and clears it from that context */ public static Optional popFrom(final RestRequest request, final AttributeKey attribute) { - if (request.getHttpChannel() instanceof Netty4HttpChannel) { - Channel nettyChannel = ((Netty4HttpChannel) request.getHttpChannel()).getNettyChannel(); - return Optional.ofNullable(nettyChannel.attr(attribute).getAndSet(null)); + final HttpChannel httpChannel = request.getHttpChannel(); + if (httpChannel != null) { + return httpChannel.get("channel", Channel.class).map(channel -> channel.attr(attribute).getAndSet(null)); + } else { + return Optional.empty(); } - return Optional.empty(); } /** @@ -40,9 +51,9 @@ public static Optional peekFrom(final ChannelHandlerContext ctx, final At * Clears an attribute value from the channel handler context */ public static void clearAttribute(final RestRequest request, final AttributeKey attribute) { - if (request.getHttpChannel() instanceof Netty4HttpChannel) { - Channel nettyChannel = ((Netty4HttpChannel) request.getHttpChannel()).getNettyChannel(); - nettyChannel.attr(attribute).set(null); + final HttpChannel httpChannel = request.getHttpChannel(); + if (httpChannel != null) { + httpChannel.get("channel", Channel.class).ifPresent(channel -> channel.attr(attribute).set(null)); } } diff --git a/src/main/java/org/opensearch/security/filter/NettyRequest.java b/src/main/java/org/opensearch/security/filter/NettyRequest.java index 7b65e4e0de..c827ddc779 100644 --- a/src/main/java/org/opensearch/security/filter/NettyRequest.java +++ b/src/main/java/org/opensearch/security/filter/NettyRequest.java @@ -14,12 +14,17 @@ import java.net.InetSocketAddress; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.TreeMap; import javax.net.ssl.SSLEngine; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; + import org.opensearch.http.netty4.Netty4HttpChannel; import org.opensearch.rest.RestRequest.Method; import org.opensearch.rest.RestUtils; @@ -34,6 +39,7 @@ public class NettyRequest implements SecurityRequest { protected final HttpRequest underlyingRequest; protected final Netty4HttpChannel underlyingChannel; + protected final Supplier parameters = Suppliers.memoize(() -> new CheckedAccessMap(params(uri()))); NettyRequest(final HttpRequest request, final Netty4HttpChannel channel) { this.underlyingRequest = request; @@ -82,7 +88,12 @@ public String uri() { @Override public Map params() { - return params(underlyingRequest.uri()); + return parameters.get(); + } + + @Override + public Set getUnconsumedParams() { + return parameters.get().accessedKeys(); } private static Map params(String uri) { @@ -100,4 +111,26 @@ private static Map params(String uri) { return params; } + + /** Records access of any keys if explicitly requested from this map */ + private static class CheckedAccessMap extends HashMap { + private final Set accessedKeys = new HashSet<>(); + + public CheckedAccessMap(final Map map) { + super(map); + } + + @Override + public String get(final Object key) { + // Never noticed this about java's map interface the getter is not generic + if (key instanceof String) { + accessedKeys.add((String) key); + } + return super.get(key); + } + + public Set accessedKeys() { + return accessedKeys; + } + } } diff --git a/src/main/java/org/opensearch/security/filter/OpenSearchRequest.java b/src/main/java/org/opensearch/security/filter/OpenSearchRequest.java index 80ede8b2c1..7cca8111c9 100644 --- a/src/main/java/org/opensearch/security/filter/OpenSearchRequest.java +++ b/src/main/java/org/opensearch/security/filter/OpenSearchRequest.java @@ -12,12 +12,13 @@ package org.opensearch.security.filter; import java.net.InetSocketAddress; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import javax.net.ssl.SSLEngine; -import org.opensearch.http.netty4.Netty4HttpChannel; import org.opensearch.rest.RestRequest; import org.opensearch.rest.RestRequest.Method; @@ -41,21 +42,11 @@ public Map> getHeaders() { @Override public SSLEngine getSSLEngine() { - if (underlyingRequest == null - || underlyingRequest.getHttpChannel() == null - || !(underlyingRequest.getHttpChannel() instanceof Netty4HttpChannel)) { + if (underlyingRequest == null || underlyingRequest.getHttpChannel() == null) { 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; + return underlyingRequest.getHttpChannel().get("ssl_http", SslHandler.class).map(SslHandler::engine).orElse(null); } @Override @@ -80,7 +71,18 @@ public String uri() { @Override public Map params() { - return underlyingRequest.params(); + return new HashMap<>(underlyingRequest.params()) { + @Override + public String get(Object key) { + return underlyingRequest.param((String) key); + } + }; + } + + @Override + public Set getUnconsumedParams() { + // params() Map consumes explict parameter access + return Set.of(); } /** Gets access to the underlying request object */ diff --git a/src/main/java/org/opensearch/security/filter/SecurityRequest.java b/src/main/java/org/opensearch/security/filter/SecurityRequest.java index 4c7ea27a87..d3741585ac 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRequest.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRequest.java @@ -15,6 +15,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; import javax.net.ssl.SSLEngine; @@ -49,4 +50,7 @@ default String header(final String headerName) { /** The parameters associated with this request */ Map params(); + + /** The list of parameters that have been accessed but not recorded as being consumed */ + Set getUnconsumedParams(); } diff --git a/src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java b/src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java index 66744d01dd..241c9009f4 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRequestChannel.java @@ -14,13 +14,13 @@ 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 + * When a request is received by the security plugin this governs getting information about the request and complete with a response */ public interface SecurityRequestChannel extends SecurityRequest { /** Associate a response with this channel */ void queueForSending(final SecurityResponse response); - /** Acess the queued response */ + /** Access the queued response */ Optional getQueuedResponse(); } diff --git a/src/main/java/org/opensearch/security/filter/SecurityResponse.java b/src/main/java/org/opensearch/security/filter/SecurityResponse.java index 0dc833a440..1c6146d0e1 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityResponse.java +++ b/src/main/java/org/opensearch/security/filter/SecurityResponse.java @@ -12,11 +12,15 @@ package org.opensearch.security.filter; import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.http.HttpHeaders; import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.BytesRestResponse; import org.opensearch.rest.RestResponse; @@ -26,26 +30,62 @@ 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 Map> headers; private final String body; + private final String contentType; public SecurityResponse(final int status, final Exception e) { this.status = status; - this.headers = CONTENT_TYPE_APP_JSON; this.body = generateFailureMessage(e); + this.contentType = XContentType.JSON.mediaType(); + } + + public SecurityResponse(final int status, String body) { + this.status = status; + this.body = body; + this.contentType = null; } public SecurityResponse(final int status, final Map headers, final String body) { this.status = status; - this.headers = headers; + populateHeaders(headers); + this.body = body; + this.contentType = null; + } + + public SecurityResponse(final int status, final Map headers, final String body, String contentType) { + this.status = status; this.body = body; + this.contentType = contentType; + populateHeaders(headers); + } + + private void populateHeaders(Map headers) { + if (headers != null) { + headers.entrySet().forEach(entry -> addHeader(entry.getKey(), entry.getValue())); + } + } + + /** + * Add a custom header. + */ + public void addHeader(String name, String value) { + if (headers == null) { + headers = new HashMap<>(2); + } + List header = headers.get(name); + if (header == null) { + header = new ArrayList<>(); + headers.put(name, header); + } + header.add(value); } public int getStatus() { return status; } - public Map getHeaders() { + public Map> getHeaders() { return headers; } @@ -54,9 +94,14 @@ public String getBody() { } public RestResponse asRestResponse() { - final RestResponse restResponse = new BytesRestResponse(RestStatus.fromCode(getStatus()), getBody()); + final RestResponse restResponse; + if (this.contentType != null) { + restResponse = new BytesRestResponse(RestStatus.fromCode(getStatus()), this.contentType, getBody()); + } else { + restResponse = new BytesRestResponse(RestStatus.fromCode(getStatus()), getBody()); + } if (getHeaders() != null) { - getHeaders().forEach(restResponse::addHeader); + getHeaders().entrySet().forEach(entry -> { entry.getValue().forEach(value -> restResponse.addHeader(entry.getKey(), value)); }); } return restResponse; } diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index e4d087cfe3..420211f29b 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -55,6 +55,7 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.securityconf.impl.AllowlistingSettings; import org.opensearch.security.securityconf.impl.WhitelistingSettings; +import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.ssl.util.SSLRequestHelper; @@ -69,9 +70,6 @@ import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.http.SecurityHttpServerTransport.CONTEXT_TO_RESTORE; -import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE; -import static org.opensearch.security.http.SecurityHttpServerTransport.IS_AUTHENTICATED; public class SecurityRestFilter { @@ -127,15 +125,18 @@ public AuthczRestHandler(RestHandler original, AdminDNs adminDNs) { @Override public void handleRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { - final Optional maybeSavedResponse = NettyAttribute.popFrom(request, EARLY_RESPONSE); + final Optional maybeSavedResponse = NettyAttribute.popFrom( + request, + Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE + ); if (maybeSavedResponse.isPresent()) { - NettyAttribute.clearAttribute(request, CONTEXT_TO_RESTORE); - NettyAttribute.clearAttribute(request, IS_AUTHENTICATED); + NettyAttribute.clearAttribute(request, Netty4HttpRequestHeaderVerifier.CONTEXT_TO_RESTORE); + NettyAttribute.clearAttribute(request, Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED); channel.sendResponse(maybeSavedResponse.get().asRestResponse()); return; } - NettyAttribute.popFrom(request, CONTEXT_TO_RESTORE).ifPresent(storedContext -> { + NettyAttribute.popFrom(request, Netty4HttpRequestHeaderVerifier.CONTEXT_TO_RESTORE).ifPresent(storedContext -> { // X_OPAQUE_ID will be overritten on restore - save to apply after restoring the saved context final String xOpaqueId = threadContext.getHeader(Task.X_OPAQUE_ID); storedContext.restore(); @@ -144,10 +145,17 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c } }); + NettyAttribute.popFrom(request, Netty4HttpRequestHeaderVerifier.UNCONSUMED_PARAMS).ifPresent(unconsumedParams -> { + for (String unconsumedParam : unconsumedParams) { + // Consume the parameter on the RestRequest + request.param(unconsumedParam); + } + }); + final SecurityRequestChannel requestChannel = SecurityRequestFactory.from(request, channel); // Authenticate request - if (!NettyAttribute.popFrom(request, IS_AUTHENTICATED).orElse(false)) { + if (!NettyAttribute.popFrom(request, Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED).orElse(false)) { // we aren't authenticated so we should skip this step checkAndAuthenticateRequest(requestChannel); } @@ -245,7 +253,7 @@ void authorizeRequest(RestHandler original, SecurityRequestChannel request, User } log.debug(err); - request.queueForSending(new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, null, err)); + request.queueForSending(new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, err)); return; } } @@ -288,7 +296,7 @@ public void checkAndAuthenticateRequest(SecurityRequestChannel requestChannel) t } catch (SSLPeerUnverifiedException e) { log.error("No ssl info", e); auditLog.logSSLException(requestChannel, e); - requestChannel.queueForSending(new SecurityResponse(HttpStatus.SC_FORBIDDEN, new Exception("No ssl info"))); + requestChannel.queueForSending(new SecurityResponse(HttpStatus.SC_FORBIDDEN, e)); return; } diff --git a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java index ff07db147e..4aec26db3d 100644 --- a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java @@ -68,7 +68,11 @@ public AuthCredentials extractCredentials(final SecurityRequest request, final T @Override public Optional reRequestAuthentication(final SecurityRequest request, AuthCredentials creds) { return Optional.of( - new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, Map.of("WWW-Authenticate", "Basic realm=\"OpenSearch Security\""), "") + new SecurityResponse( + HttpStatus.SC_UNAUTHORIZED, + Map.of("WWW-Authenticate", "Basic realm=\"OpenSearch Security\""), + "Unauthorized" + ) ); } diff --git a/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java b/src/main/java/org/opensearch/security/http/NonSslHttpServerTransport.java similarity index 75% rename from src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java rename to src/main/java/org/opensearch/security/http/NonSslHttpServerTransport.java index f37ebb48e8..d0d1d9f09f 100644 --- a/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java +++ b/src/main/java/org/opensearch/security/http/NonSslHttpServerTransport.java @@ -33,22 +33,17 @@ import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.http.HttpHandlingSettings; import org.opensearch.http.netty4.Netty4HttpServerTransport; -import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; -import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; import org.opensearch.telemetry.tracing.Tracer; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.SharedGroupFactory; import io.netty.channel.Channel; import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelInboundHandlerAdapter; -public class SecurityNonSslHttpServerTransport extends Netty4HttpServerTransport { - - private final ChannelInboundHandlerAdapter headerVerifier; - - public SecurityNonSslHttpServerTransport( +public class NonSslHttpServerTransport extends SecureNetty4HttpServerTransport { + public NonSslHttpServerTransport( final Settings settings, final NetworkService networkService, final BigArrays bigArrays, @@ -57,8 +52,8 @@ public SecurityNonSslHttpServerTransport( final Dispatcher dispatcher, final ClusterSettings clusterSettings, final SharedGroupFactory sharedGroupFactory, - final Tracer tracer, - final SecurityRestFilter restFilter + final SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider, + final Tracer tracer ) { super( settings, @@ -69,9 +64,9 @@ public SecurityNonSslHttpServerTransport( dispatcher, clusterSettings, sharedGroupFactory, + secureHttpTransportSettingsProvider, tracer ); - headerVerifier = new Netty4HttpRequestHeaderVerifier(restFilter, threadPool, settings); } @Override @@ -90,14 +85,4 @@ protected void initChannel(Channel ch) throws Exception { super.initChannel(ch); } } - - @Override - protected ChannelInboundHandlerAdapter createHeaderVerifier() { - return headerVerifier; - } - - @Override - protected ChannelInboundHandlerAdapter createDecompressor() { - return new Netty4ConditionalDecompressor(); - } } diff --git a/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java b/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java deleted file mode 100644 index c5fbbfbbc6..0000000000 --- a/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.http; - -import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.BigArrays; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.security.filter.SecurityResponse; -import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.SslExceptionHandler; -import org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport; -import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; -import org.opensearch.telemetry.tracing.Tracer; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.SharedGroupFactory; - -import io.netty.util.AttributeKey; - -public class SecurityHttpServerTransport extends SecuritySSLNettyHttpServerTransport { - - public static final AttributeKey EARLY_RESPONSE = AttributeKey.newInstance("opensearch-http-early-response"); - public static final AttributeKey CONTEXT_TO_RESTORE = AttributeKey.newInstance( - "opensearch-http-request-thread-context" - ); - public static final AttributeKey SHOULD_DECOMPRESS = AttributeKey.newInstance("opensearch-http-should-decompress"); - public static final AttributeKey IS_AUTHENTICATED = AttributeKey.newInstance("opensearch-http-is-authenticated"); - - public SecurityHttpServerTransport( - final Settings settings, - final NetworkService networkService, - final BigArrays bigArrays, - final ThreadPool threadPool, - final SecurityKeyStore odsks, - final SslExceptionHandler sslExceptionHandler, - final NamedXContentRegistry namedXContentRegistry, - final ValidatingDispatcher dispatcher, - final ClusterSettings clusterSettings, - SharedGroupFactory sharedGroupFactory, - Tracer tracer, - SecurityRestFilter restFilter - ) { - super( - settings, - networkService, - bigArrays, - threadPool, - odsks, - namedXContentRegistry, - dispatcher, - sslExceptionHandler, - clusterSettings, - sharedGroupFactory, - tracer, - restFilter - ); - } -} diff --git a/src/main/java/org/opensearch/security/httpclient/HttpClient.java b/src/main/java/org/opensearch/security/httpclient/HttpClient.java index 466dac2a82..43b5107b70 100644 --- a/src/main/java/org/opensearch/security/httpclient/HttpClient.java +++ b/src/main/java/org/opensearch/security/httpclient/HttpClient.java @@ -13,6 +13,8 @@ import java.io.Closeable; import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; import java.security.KeyManagementException; import java.security.KeyStore; @@ -176,14 +178,8 @@ private HttpClient( this.supportedCipherSuites = supportedCipherSuites; this.keystoreAlias = keystoreAlias; - HttpHost[] hosts = Arrays.stream(servers) - .map(s -> s.split(":")) - .map(s -> new HttpHost(ssl ? "https" : "http", s[0], Integer.parseInt(s[1]))) - .collect(Collectors.toList()) - .toArray(new HttpHost[0]); - + HttpHost[] hosts = createHosts(servers); RestClientBuilder builder = RestClient.builder(hosts); - // builder.setMaxRetryTimeoutMillis(10000); builder.setFailureListener(new RestClient.FailureListener() { @Override @@ -208,6 +204,24 @@ public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpCli rclient = new RestHighLevelClient(builder); } + private HttpHost[] createHosts(String[] servers) { + return Arrays.stream(servers).map(server -> { + try { + server = addSchemeBasedOnSSL(server); + URI uri = new URI(server); + return new HttpHost(uri.getScheme(), uri.getHost(), uri.getPort()); + } catch (URISyntaxException e) { + return null; + } + }).filter(Objects::nonNull).collect(Collectors.toList()).toArray(HttpHost[]::new); + } + + private String addSchemeBasedOnSSL(String server) { + server = server.replaceAll("https://|http://", ""); + String protocol = ssl ? "https://" : "http://"; + return protocol.concat(server); + } + public boolean index(final String content, final String index, final String type, final boolean refresh) { try { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 1d09932131..872c2c37f2 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -81,6 +81,7 @@ import org.opensearch.core.common.transport.TransportAddress; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.index.reindex.ReindexAction; +import org.opensearch.script.mustache.RenderSearchTemplateAction; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.configuration.ConfigurationRepository; @@ -89,6 +90,7 @@ import org.opensearch.security.securityconf.ConfigModel; import org.opensearch.security.securityconf.DynamicConfigModel; import org.opensearch.security.securityconf.SecurityRoles; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; @@ -191,7 +193,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { this.dcm = dcm; } - private SecurityRoles getSecurityRoles(Set roles) { + public SecurityRoles getSecurityRoles(Set roles) { return configModel.getSecurityRoles().filter(roles); } @@ -619,6 +621,10 @@ public String dashboardsOpenSearchRole() { return dcm.getDashboardsOpenSearchRole(); } + public List getSignInOptions() { + return dcm.getSignInOptions(); + } + private Set evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { // --- check inner bulk requests final Set additionalPermissionsRequired = new HashSet<>(); @@ -696,8 +702,7 @@ public static boolean isClusterPerm(String action0) { || (action0.startsWith(MultiSearchAction.NAME)) || (action0.equals(MultiTermVectorsAction.NAME)) || (action0.equals(ReindexAction.NAME)) - - ); + || (action0.equals(RenderSearchTemplateAction.NAME))); } @SuppressWarnings("unchecked") diff --git a/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java b/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java index 3ebfbce29b..d7aeb776c7 100644 --- a/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java +++ b/src/main/java/org/opensearch/security/resolver/IndexResolverReplacer.java @@ -56,6 +56,7 @@ import org.opensearch.action.admin.indices.datastream.CreateDataStreamAction; import org.opensearch.action.admin.indices.mapping.put.PutMappingRequest; import org.opensearch.action.admin.indices.resolve.ResolveIndexAction; +import org.opensearch.action.admin.indices.shrink.ResizeRequest; import org.opensearch.action.admin.indices.template.put.PutComponentTemplateAction; import org.opensearch.action.bulk.BulkRequest; import org.opensearch.action.bulk.BulkShardRequest; @@ -777,6 +778,10 @@ private boolean getOrReplaceAllIndices(final Object request, final IndicesProvid return false; } ((CreateIndexRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); + } else if (request instanceof ResizeRequest) { + // clone or shrink operations + provider.provide(((ResizeRequest) request).indices(), request, true); + provider.provide(((ResizeRequest) request).getTargetIndexRequest().indices(), request, true); } else if (request instanceof CreateDataStreamAction.Request) { provider.provide(((CreateDataStreamAction.Request) request).indices(), request, false); } else if (request instanceof ReindexRequest) { diff --git a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index 2b286d0c3d..3401ac71e8 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -50,15 +50,19 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class DashboardsInfoAction extends BaseRestHandler { private static final List routes = ImmutableList.builder() .addAll( - addRoutesPrefix(ImmutableList.of(new Route(GET, "/dashboardsinfo"), new Route(POST, "/dashboardsinfo")), "/_plugins/_security") + addRoutesPrefix(ImmutableList.of(new Route(GET, "/dashboardsinfo"), new Route(POST, "/dashboardsinfo")), PLUGIN_ROUTE_PREFIX) + ) + .addAll( + addRoutesPrefix(ImmutableList.of(new Route(GET, "/kibanainfo"), new Route(POST, "/kibanainfo")), LEGACY_PLUGIN_ROUTE_PREFIX) ) - .addAll(addRoutesPrefix(ImmutableList.of(new Route(GET, "/kibanainfo"), new Route(POST, "/kibanainfo")), "/_opendistro/_security")) .build(); private final Logger log = LogManager.getLogger(this.getClass()); @@ -108,6 +112,7 @@ public void accept(RestChannel channel) throws Exception { builder.field("multitenancy_enabled", evaluator.multitenancyEnabled()); builder.field("private_tenant_enabled", evaluator.privateTenantEnabled()); builder.field("default_tenant", evaluator.dashboardsDefaultTenant()); + builder.field("sign_in_options", evaluator.getSignInOptions()); builder.field( "password_validation_error_message", client.settings().get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, DEFAULT_PASSWORD_MESSAGE) diff --git a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java index 1b7e788dae..3c57773417 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java @@ -44,13 +44,15 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class SecurityHealthAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(GET, "/health"), new Route(POST, "/health")), - "/_opendistro/_security", - "/_plugins/_security" + LEGACY_PLUGIN_ROUTE_PREFIX, + PLUGIN_ROUTE_PREFIX ); private final BackendRegistry registry; diff --git a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java index 9300cf72f2..64075d5d0e 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java @@ -57,13 +57,15 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class SecurityInfoAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(GET, "/authinfo"), new Route(POST, "/authinfo")), - "/_opendistro/_security", - "/_plugins/_security" + LEGACY_PLUGIN_ROUTE_PREFIX, + PLUGIN_ROUTE_PREFIX ); private final Logger log = LogManager.getLogger(this.getClass()); @@ -88,6 +90,10 @@ public List routes() { @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + final boolean verbose = request.paramAsBoolean("verbose", false); + // need to consume `auth_type` param, without which a 500 is thrown on front-end + final String authType = request.param("auth_type", ""); + return new RestChannelConsumer() { @Override @@ -97,8 +103,6 @@ public void accept(RestChannel channel) throws Exception { try { - final boolean verbose = request.paramAsBoolean("verbose", false); - final X509Certificate[] certs = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PEER_CERTIFICATES); final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index 1b0bdd7f8e..bd911463d4 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -61,13 +61,15 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.dlic.rest.support.Utils.LEGACY_PLUGIN_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_ROUTE_PREFIX; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class TenantInfoAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( ImmutableList.of(new Route(GET, "/tenantinfo"), new Route(POST, "/tenantinfo")), - "/_opendistro/_security", - "/_plugins/_security" + LEGACY_PLUGIN_ROUTE_PREFIX, + PLUGIN_ROUTE_PREFIX ); private final Logger log = LogManager.getLogger(this.getClass()); diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index 3650057d63..e35fb40a24 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -542,6 +542,26 @@ public boolean impliesTypePermGlobal( roles.stream().forEach(p -> ipatterns.addAll(p.getIpatterns())); return ConfigModelV6.impliesTypePerm(ipatterns, resolved, user, actions, resolver, cs); } + + @Override + public boolean isPermittedOnSystemIndex(String indexName) { + boolean isPatternMatched = false; + boolean isPermitted = false; + for (SecurityRole role : roles) { + for (IndexPattern ip : role.getIpatterns()) { + WildcardMatcher wildcardMatcher = WildcardMatcher.from(ip.indexPattern); + if (wildcardMatcher.test(indexName)) { + isPatternMatched = true; + } + for (TypePerm tp : ip.getTypePerms()) { + if (tp.perms.contains(ConfigConstants.SYSTEM_INDEX_PERMISSION)) { + isPermitted = true; + } + } + } + } + return isPatternMatched && isPermitted; + } } public static class SecurityRole { diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 473e224538..5c776dffa9 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -563,6 +563,24 @@ private boolean containsDlsFlsConfig() { return false; } + + @Override + public boolean isPermittedOnSystemIndex(String indexName) { + boolean isPatternMatched = false; + boolean isPermitted = false; + for (SecurityRole role : roles) { + for (IndexPattern ip : role.getIpatterns()) { + WildcardMatcher wildcardMatcher = WildcardMatcher.from(ip.indexPattern); + if (wildcardMatcher.test(indexName)) { + isPatternMatched = true; + } + if (ip.perms.contains(ConfigConstants.SYSTEM_INDEX_PERMISSION)) { + isPermitted = true; + } + } + } + return isPatternMatched && isPermitted; + } } public static class SecurityRole { diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index ed61481885..f046b4c114 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -315,7 +315,6 @@ public void onChange(Map> typeToConfig) { } initialized.set(true); - } private static ConfigV6 getConfigV6(SecurityDynamicConfiguration sdc) { diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java index e3d10878da..064f555a75 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java @@ -52,6 +52,7 @@ import org.opensearch.security.http.HTTPClientCertAuthenticator; import org.opensearch.security.http.HTTPProxyAuthenticator; import org.opensearch.security.http.proxy.HTTPExtendedProxyAuthenticator; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; public abstract class DynamicConfigModel { @@ -105,6 +106,8 @@ public abstract class DynamicConfigModel { public abstract Multimap> getAuthBackendClientBlockRegistries(); + public abstract List getSignInOptions(); + public abstract Settings getDynamicOnBehalfOfSettings(); protected final Map authImplMap = new HashMap<>(); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java index e5308aa574..c7edaf938c 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java @@ -54,6 +54,7 @@ import org.opensearch.security.auth.HTTPAuthenticator; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.securityconf.impl.v6.ConfigV6; import org.opensearch.security.securityconf.impl.v6.ConfigV6.Authc; import org.opensearch.security.securityconf.impl.v6.ConfigV6.AuthcDomain; @@ -68,8 +69,6 @@ public class DynamicConfigModelV6 extends DynamicConfigModel { private final Path configPath; private SortedSet restAuthDomains; private Set restAuthorizers; - private SortedSet transportAuthDomains; - private Set transportAuthorizers; private List destroyableComponents; private final InternalAuthenticationBackend iab; @@ -207,6 +206,11 @@ public Multimap> getAuthBackendClientBlockRe return Multimaps.unmodifiableMultimap(authBackendClientBlockRegistries); } + @Override + public List getSignInOptions() { + return config.dynamic.kibana.sign_in_options; + } + @Override public Settings getDynamicOnBehalfOfSettings() { return Settings.EMPTY; @@ -216,8 +220,6 @@ private void buildAAA() { final SortedSet restAuthDomains0 = new TreeSet<>(); final Set restAuthorizers0 = new HashSet<>(); - final SortedSet transportAuthDomains0 = new TreeSet<>(); - final Set transportAuthorizers0 = new HashSet<>(); final List destroyableComponents0 = new LinkedList<>(); final List ipAuthFailureListeners0 = new ArrayList<>(); final Multimap authBackendFailureListeners0 = ArrayListMultimap.create(); @@ -229,9 +231,8 @@ private void buildAAA() { for (final Entry ad : authzDyn.getDomains().entrySet()) { final boolean enabled = ad.getValue().enabled; final boolean httpEnabled = enabled && ad.getValue().http_enabled; - final boolean transportEnabled = enabled && ad.getValue().transport_enabled; - if (httpEnabled || transportEnabled) { + if (httpEnabled) { try { final String authzBackendClazz = ad.getValue().authorization_backend.type; @@ -264,10 +265,6 @@ private void buildAAA() { restAuthorizers0.add(authorizationBackend); } - if (transportEnabled) { - transportAuthorizers0.add(authorizationBackend); - } - if (authorizationBackend instanceof Destroyable) { destroyableComponents0.add((Destroyable) authorizationBackend); } @@ -282,9 +279,8 @@ private void buildAAA() { for (final Entry ad : authcDyn.getDomains().entrySet()) { final boolean enabled = ad.getValue().enabled; final boolean httpEnabled = enabled && ad.getValue().http_enabled; - final boolean transportEnabled = enabled && ad.getValue().transport_enabled; - if (httpEnabled || transportEnabled) { + if (httpEnabled) { try { AuthenticationBackend authenticationBackend; final String authBackendClazz = ad.getValue().authentication_backend.type; @@ -343,10 +339,6 @@ private void buildAAA() { restAuthDomains0.add(_ad); } - if (transportEnabled) { - transportAuthDomains0.add(_ad); - } - if (httpAuthenticator instanceof Destroyable) { destroyableComponents0.add((Destroyable) httpAuthenticator); } @@ -365,9 +357,7 @@ private void buildAAA() { List originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); - transportAuthDomains = Collections.unmodifiableSortedSet(transportAuthDomains0); restAuthorizers = Collections.unmodifiableSet(restAuthorizers0); - transportAuthorizers = Collections.unmodifiableSet(transportAuthorizers0); destroyableComponents = Collections.unmodifiableList(destroyableComponents0); diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 0de83f2e2e..ca237bc054 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -60,6 +60,7 @@ import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; import org.opensearch.security.configuration.ClusterInfoHolder; import org.opensearch.security.http.OnBehalfOfAuthenticator; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.securityconf.impl.v7.ConfigV7; import org.opensearch.security.securityconf.impl.v7.ConfigV7.Authc; import org.opensearch.security.securityconf.impl.v7.ConfigV7.AuthcDomain; @@ -76,8 +77,6 @@ public class DynamicConfigModelV7 extends DynamicConfigModel { private final Path configPath; private SortedSet restAuthDomains; private Set restAuthorizers; - private SortedSet transportAuthDomains; - private Set transportAuthorizers; private List destroyableComponents; private final InternalAuthenticationBackend iab; @@ -223,6 +222,11 @@ public Multimap> getAuthBackendClientBlockRe return Multimaps.unmodifiableMultimap(authBackendClientBlockRegistries); } + @Override + public List getSignInOptions() { + return config.dynamic.kibana.sign_in_options; + } + @Override public Settings getDynamicOnBehalfOfSettings() { return Settings.builder() @@ -234,8 +238,6 @@ private void buildAAA() { final SortedSet restAuthDomains0 = new TreeSet<>(); final Set restAuthorizers0 = new HashSet<>(); - final SortedSet transportAuthDomains0 = new TreeSet<>(); - final Set transportAuthorizers0 = new HashSet<>(); final List destroyableComponents0 = new LinkedList<>(); final List ipAuthFailureListeners0 = new ArrayList<>(); final Multimap authBackendFailureListeners0 = ArrayListMultimap.create(); @@ -246,9 +248,8 @@ private void buildAAA() { for (final Entry ad : authzDyn.getDomains().entrySet()) { final boolean httpEnabled = ad.getValue().http_enabled; - final boolean transportEnabled = ad.getValue().transport_enabled; - if (httpEnabled || transportEnabled) { + if (httpEnabled) { try { final String authzBackendClazz = ad.getValue().authorization_backend.type; @@ -281,10 +282,6 @@ private void buildAAA() { restAuthorizers0.add(authorizationBackend); } - if (transportEnabled) { - transportAuthorizers0.add(authorizationBackend); - } - if (authorizationBackend instanceof Destroyable) { destroyableComponents0.add((Destroyable) authorizationBackend); } @@ -298,9 +295,8 @@ private void buildAAA() { for (final Entry ad : authcDyn.getDomains().entrySet()) { final boolean httpEnabled = ad.getValue().http_enabled; - final boolean transportEnabled = ad.getValue().transport_enabled; - if (httpEnabled || transportEnabled) { + if (httpEnabled) { try { AuthenticationBackend authenticationBackend; final String authBackendClazz = ad.getValue().authentication_backend.type; @@ -359,10 +355,6 @@ private void buildAAA() { restAuthDomains0.add(_ad); } - if (transportEnabled) { - transportAuthDomains0.add(_ad); - } - if (httpAuthenticator instanceof Destroyable) { destroyableComponents0.add((Destroyable) httpAuthenticator); } @@ -398,9 +390,7 @@ private void buildAAA() { List originalDestroyableComponents = destroyableComponents; restAuthDomains = Collections.unmodifiableSortedSet(restAuthDomains0); - transportAuthDomains = Collections.unmodifiableSortedSet(transportAuthDomains0); restAuthorizers = Collections.unmodifiableSet(restAuthorizers0); - transportAuthorizers = Collections.unmodifiableSet(transportAuthorizers0); destroyableComponents = Collections.unmodifiableList(destroyableComponents0); diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index 079853d581..fb25e1a21f 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -96,4 +96,6 @@ Set getAllPermittedIndicesForDashboards( ); SecurityRoles filter(Set roles); + + boolean isPermittedOnSystemIndex(String indexName); } 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 63d9186e1f..2a25ad8795 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java @@ -20,6 +20,7 @@ import org.apache.http.HttpStatus; import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.rest.RestStatus; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityResponse; @@ -113,7 +114,7 @@ public Optional checkRequestIsAllowed(final SecurityRequest re // if allowlisting is enabled but the request is not allowlisted, then return false, otherwise true. if (this.enabled && !requestIsAllowlisted(request)) { return Optional.of( - new SecurityResponse(HttpStatus.SC_FORBIDDEN, SecurityResponse.CONTENT_TYPE_APP_JSON, generateFailureMessage(request)) + new SecurityResponse(HttpStatus.SC_FORBIDDEN, null, generateFailureMessage(request), XContentType.JSON.mediaType()) ); } return Optional.empty(); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index 4e5e2de496..8a51686225 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -27,14 +27,17 @@ package org.opensearch.security.securityconf.impl; +import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.function.Predicate; import java.util.stream.Collectors; +import com.google.common.collect.ImmutableMap; + import org.opensearch.security.auditlog.config.AuditConfig; import org.opensearch.security.securityconf.impl.v6.ActionGroupsV6; import org.opensearch.security.securityconf.impl.v6.ConfigV6; @@ -50,21 +53,39 @@ public enum CType { - INTERNALUSERS(toMap(1, InternalUserV6.class, 2, InternalUserV7.class)), - ACTIONGROUPS(toMap(0, List.class, 1, ActionGroupsV6.class, 2, ActionGroupsV7.class)), - CONFIG(toMap(1, ConfigV6.class, 2, ConfigV7.class)), - ROLES(toMap(1, RoleV6.class, 2, RoleV7.class)), - ROLESMAPPING(toMap(1, RoleMappingsV6.class, 2, RoleMappingsV7.class)), - TENANTS(toMap(2, TenantV7.class)), - NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class)), - WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class)), - ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class)), - AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class)); + ACTIONGROUPS(toMap(0, List.class, 1, ActionGroupsV6.class, 2, ActionGroupsV7.class), "action_groups.yml", false), + ALLOWLIST(toMap(1, AllowlistingSettings.class, 2, AllowlistingSettings.class), "allowlist.yml", true), + AUDIT(toMap(1, AuditConfig.class, 2, AuditConfig.class), "audit.yml", true), + CONFIG(toMap(1, ConfigV6.class, 2, ConfigV7.class), "config.yml", false), + INTERNALUSERS(toMap(1, InternalUserV6.class, 2, InternalUserV7.class), "internal_users.yml", false), + NODESDN(toMap(1, NodesDn.class, 2, NodesDn.class), "nodes_dn.yml", true), + ROLES(toMap(1, RoleV6.class, 2, RoleV7.class), "roles.yml", false), + ROLESMAPPING(toMap(1, RoleMappingsV6.class, 2, RoleMappingsV7.class), "roles_mapping.yml", false), + TENANTS(toMap(2, TenantV7.class), "tenants.yml", false), + WHITELIST(toMap(1, WhitelistingSettings.class, 2, WhitelistingSettings.class), "whitelist.yml", true); + + public static final List REQUIRED_CONFIG_FILES = Arrays.stream(CType.values()) + .filter(Predicate.not(CType::emptyIfMissing)) + .collect(Collectors.toList()); + + public static final List NOT_REQUIRED_CONFIG_FILES = Arrays.stream(CType.values()) + .filter(CType::emptyIfMissing) + .collect(Collectors.toList()); - private Map> implementations; + private final Map> implementations; - private CType(Map> implementations) { + private final String configFileName; + + private final boolean emptyIfMissing; + + private CType(Map> implementations, final String configFileName, final boolean emptyIfMissing) { this.implementations = implementations; + this.configFileName = configFileName; + this.emptyIfMissing = emptyIfMissing; + } + + public boolean emptyIfMissing() { + return emptyIfMissing; } public Map> getImplementationClass() { @@ -80,18 +101,26 @@ public String toLCString() { } public static Set lcStringValues() { - return Arrays.stream(CType.values()).map(c -> c.toLCString()).collect(Collectors.toSet()); + return Arrays.stream(CType.values()).map(CType::toLCString).collect(Collectors.toSet()); } public static Set fromStringValues(String[] strings) { - return Arrays.stream(strings).map(c -> CType.fromString(c)).collect(Collectors.toSet()); + return Arrays.stream(strings).map(CType::fromString).collect(Collectors.toSet()); + } + + public Path configFile(final Path configDir) { + return configDir.resolve(this.configFileName); + } + + public String configFileName() { + return configFileName; } private static Map> toMap(Object... objects) { - final Map> map = new HashMap>(); + final ImmutableMap.Builder> map = ImmutableMap.builder(); for (int i = 0; i < objects.length; i = i + 2) { map.put((Integer) objects[i], (Class) objects[i + 1]); } - return Collections.unmodifiableMap(map); + return map.build(); } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/DashboardSignInOption.java b/src/main/java/org/opensearch/security/securityconf/impl/DashboardSignInOption.java new file mode 100644 index 0000000000..3d9fa20e97 --- /dev/null +++ b/src/main/java/org/opensearch/security/securityconf/impl/DashboardSignInOption.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.securityconf.impl; + +public enum DashboardSignInOption { + BASIC("basic"), + SAML("saml"), + OPENID("openid"), + ANONYMOUS("anonymous"); + + private String option; + + DashboardSignInOption(String option) { + this.option = option; + } + + public String getOption() { + return option; + } +} diff --git a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java index 938ee23c1e..83553f2de7 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -30,6 +30,7 @@ import java.io.IOException; import java.util.Collections; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -56,6 +57,8 @@ public class SecurityDynamicConfiguration implements ToXContent { @JsonIgnore private final Map centries = new HashMap<>(); + @JsonIgnore + private final Object modificationLock = new Object(); private long seqNo = -1; private long primaryTerm = -1; private CType ctype; @@ -65,6 +68,11 @@ public static SecurityDynamicConfiguration empty() { return new SecurityDynamicConfiguration(); } + @JsonIgnore + public boolean notEmpty() { + return !centries.isEmpty(); + } + public static SecurityDynamicConfiguration fromJson(String json, CType ctype, int version, long seqNo, long primaryTerm) throws IOException { return fromJson(json, ctype, version, seqNo, primaryTerm, false); @@ -158,23 +166,33 @@ void setCEntries(String key, T value) { @JsonAnyGetter public Map getCEntries() { - return centries; + synchronized (modificationLock) { + return new HashMap<>(centries); + } } @JsonIgnore public void removeHidden() { - for (Entry entry : new HashMap(centries).entrySet()) { - if (entry.getValue() instanceof Hideable && ((Hideable) entry.getValue()).isHidden()) { - centries.remove(entry.getKey()); + synchronized (modificationLock) { + final Iterator> iterator = centries.entrySet().iterator(); + while (iterator.hasNext()) { + final var entry = iterator.next(); + if (entry.getValue() instanceof Hideable && ((Hideable) entry.getValue()).isHidden()) { + iterator.remove(); + } } } } @JsonIgnore public void removeStatic() { - for (Entry entry : new HashMap(centries).entrySet()) { - if (entry.getValue() instanceof StaticDefinable && ((StaticDefinable) entry.getValue()).isStatic()) { - centries.remove(entry.getKey()); + synchronized (modificationLock) { + final Iterator> iterator = centries.entrySet().iterator(); + while (iterator.hasNext()) { + final var entry = iterator.next(); + if (entry.getValue() instanceof StaticDefinable && ((StaticDefinable) entry.getValue()).isStatic()) { + iterator.remove(); + } } } } @@ -189,20 +207,26 @@ public void clearHashes() { } public void removeOthers(String key) { - T tmp = this.centries.get(key); - this.centries.clear(); - this.centries.put(key, tmp); + synchronized (modificationLock) { + T tmp = this.centries.get(key); + this.centries.clear(); + this.centries.put(key, tmp); + } } @JsonIgnore public T putCEntry(String key, T value) { - return centries.put(key, value); + synchronized (modificationLock) { + return centries.put(key, value); + } } @JsonIgnore @SuppressWarnings("unchecked") public void putCObject(String key, Object value) { - centries.put(key, (T) value); + synchronized (modificationLock) { + centries.put(key, (T) value); + } } @JsonIgnore @@ -284,38 +308,53 @@ public SecurityDynamicConfiguration deepClone() { } } + @JsonIgnore + public SecurityDynamicConfiguration deepCloneWithRedaction() { + try { + return fromJson(DefaultObjectMapper.writeValueAsStringAndRedactSensitive(this), ctype, version, seqNo, primaryTerm); + } catch (Exception e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } + @JsonIgnore public void remove(String key) { - centries.remove(key); + synchronized (modificationLock) { + centries.remove(key); + } } @JsonIgnore public void remove(List keySet) { - keySet.stream().forEach(this::remove); + synchronized (modificationLock) { + keySet.stream().forEach(centries::remove); + } } @SuppressWarnings({ "rawtypes", "unchecked" }) public boolean add(SecurityDynamicConfiguration other) { - if (other.ctype == null || !other.ctype.equals(this.ctype)) { - return false; - } + synchronized (modificationLock) { + if (other.ctype == null || !other.ctype.equals(this.ctype)) { + return false; + } - if (other.getImplementingClass() == null || !other.getImplementingClass().equals(this.getImplementingClass())) { - return false; - } + if (other.getImplementingClass() == null || !other.getImplementingClass().equals(this.getImplementingClass())) { + return false; + } - if (other.version != this.version) { - return false; - } + if (other.version != this.version) { + return false; + } - this.centries.putAll(other.centries); - return true; + this.centries.putAll(other.centries); + return true; + } } @JsonIgnore @SuppressWarnings({ "rawtypes" }) public boolean containsAny(SecurityDynamicConfiguration other) { - return !Collections.disjoint(this.centries.keySet(), other.centries.keySet()); + return !Collections.disjoint(this.getCEntries().keySet(), other.getCEntries().keySet()); } public boolean isHidden(String resourceName) { 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 ce643477c2..4cc16a7f00 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java @@ -18,6 +18,7 @@ import org.apache.http.HttpStatus; +import org.opensearch.common.xcontent.XContentType; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityResponse; @@ -111,7 +112,7 @@ public Optional checkRequestIsAllowed(final SecurityRequest re // if whitelisting is enabled but the request is not whitelisted, then return false, otherwise true. if (this.enabled && !requestIsWhitelisted(request)) { return Optional.of( - new SecurityResponse(HttpStatus.SC_FORBIDDEN, SecurityResponse.CONTENT_TYPE_APP_JSON, generateFailureMessage(request)) + new SecurityResponse(HttpStatus.SC_FORBIDDEN, null, generateFailureMessage(request), XContentType.JSON.mediaType()) ); } return Optional.empty(); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java index 0c95e56bd1..78758e0603 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java @@ -27,8 +27,10 @@ package org.opensearch.security.securityconf.impl.v6; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Pattern; @@ -38,9 +40,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; +import org.opensearch.security.setting.DeprecatedSettings; public class ConfigV6 { @@ -97,6 +103,8 @@ public static class Kibana { public String opendistro_role = null; public String index = ".kibana"; public boolean do_not_fail_on_forbidden; + @JsonInclude(JsonInclude.Include.NON_NULL) + public List sign_in_options = Arrays.asList(DashboardSignInOption.BASIC); @Override public String toString() { @@ -110,6 +118,8 @@ public String toString() { + index + ", do_not_fail_on_forbidden=" + do_not_fail_on_forbidden + + ", sign_in_options=" + + sign_in_options + "]"; } @@ -224,8 +234,6 @@ public static class AuthcDomain { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean http_enabled = true; @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean transport_enabled = true; - @JsonInclude(JsonInclude.Include.NON_NULL) public boolean enabled = true; public int order = 0; public HttpAuthenticator http_authenticator = new HttpAuthenticator(); @@ -235,8 +243,6 @@ public static class AuthcDomain { public String toString() { return "AuthcDomain [http_enabled=" + http_enabled - + ", transport_enabled=" - + transport_enabled + ", enabled=" + enabled + ", order=" @@ -248,6 +254,31 @@ public String toString() { + "]"; } + @JsonAnySetter + public void unknownPropertiesHandler(String name, Object value) throws JsonMappingException { + switch (name) { + case "transport_enabled": + DeprecatedSettings.logCustomDeprecationMessage( + String.format( + "In AuthcDomain, using http_authenticator=%s, authentication_backend=%s", + http_authenticator, + authentication_backend + ), + name + ); + break; + default: + throw new UnrecognizedPropertyException( + null, + "Unrecognized field " + name + " present in the input data for AuthcDomain config", + null, + AuthcDomain.class, + name, + null + ); + } + } + } public static class HttpAuthenticator { @@ -337,8 +368,6 @@ public static class AuthzDomain { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean http_enabled = true; @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean transport_enabled = true; - @JsonInclude(JsonInclude.Include.NON_NULL) public boolean enabled = true; public AuthzBackend authorization_backend = new AuthzBackend(); @@ -346,8 +375,6 @@ public static class AuthzDomain { public String toString() { return "AuthzDomain [http_enabled=" + http_enabled - + ", transport_enabled=" - + transport_enabled + ", enabled=" + enabled + ", authorization_backend=" @@ -355,6 +382,27 @@ public String toString() { + "]"; } + @JsonAnySetter + public void unknownPropertiesHandler(String name, Object value) throws JsonMappingException { + switch (name) { + case "transport_enabled": + DeprecatedSettings.logCustomDeprecationMessage( + String.format("In AuthzDomain, using authorization_backend=%s", authorization_backend), + name + ); + break; + default: + throw new UnrecognizedPropertyException( + null, + "Unrecognized field " + name + " present in the input data for AuthzDomain config", + null, + AuthzDomain.class, + name, + null + ); + } + } + } public static class OnBehalfOfSettings { diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index faeb5d2432..dc9be395b1 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -27,8 +27,10 @@ package org.opensearch.security.securityconf.impl.v7; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -39,10 +41,14 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException; import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.auth.internal.InternalAuthenticationBackend; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.securityconf.impl.v6.ConfigV6; +import org.opensearch.security.setting.DeprecatedSettings; public class ConfigV7 { @@ -73,6 +79,7 @@ public ConfigV7(ConfigV6 c6) { dynamic.kibana.private_tenant_enabled = true; dynamic.kibana.default_tenant = ""; dynamic.kibana.server_username = c6.dynamic.kibana.server_username; + dynamic.kibana.sign_in_options = c6.dynamic.kibana.sign_in_options; dynamic.http = new Http(); @@ -165,6 +172,8 @@ public static class Kibana { public String server_username = "kibanaserver"; public String opendistro_role = null; public String index = ".kibana"; + @JsonInclude(JsonInclude.Include.NON_NULL) + public List sign_in_options = Arrays.asList(DashboardSignInOption.BASIC); @Override public String toString() { @@ -180,6 +189,8 @@ public String toString() { + opendistro_role + ", index=" + index + + ", sign_in_options=" + + sign_in_options + "]"; } @@ -293,7 +304,6 @@ public static class AuthcDomain { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean http_enabled = true; @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean transport_enabled = true; // public boolean enabled= true; public int order = 0; public HttpAuthenticator http_authenticator = new HttpAuthenticator(); @@ -307,10 +317,8 @@ public AuthcDomain() { public AuthcDomain(ConfigV6.AuthcDomain v6) { super(); http_enabled = v6.http_enabled && v6.enabled; - transport_enabled = v6.transport_enabled && v6.enabled; // if(v6.enabled)vv { // http_enabled = true; - // transport_enabled = true; // } order = v6.order; http_authenticator = new HttpAuthenticator(v6.http_authenticator); @@ -322,8 +330,6 @@ public AuthcDomain(ConfigV6.AuthcDomain v6) { public String toString() { return "AuthcDomain [http_enabled=" + http_enabled - + ", transport_enabled=" - + transport_enabled + ", order=" + order + ", http_authenticator=" @@ -335,6 +341,31 @@ public String toString() { + "]"; } + @JsonAnySetter + public void unknownPropertiesHandler(String name, Object value) throws JsonMappingException { + switch (name) { + case "transport_enabled": + DeprecatedSettings.logCustomDeprecationMessage( + String.format( + "In AuthcDomain, using http_authenticator=%s, authentication_backend=%s", + http_authenticator, + authentication_backend + ), + name + ); + break; + default: + throw new UnrecognizedPropertyException( + null, + "Unrecognized field " + name + " present in the input data for AuthcDomain config", + null, + AuthcDomain.class, + name, + null + ); + } + } + } public static class HttpAuthenticator { @@ -451,8 +482,6 @@ public String toString() { public static class AuthzDomain { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean http_enabled = true; - @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean transport_enabled = true; public AuthzBackend authorization_backend = new AuthzBackend(); public String description; @@ -462,7 +491,6 @@ public AuthzDomain() { public AuthzDomain(ConfigV6.AuthzDomain v6) { http_enabled = v6.http_enabled && v6.enabled; - transport_enabled = v6.transport_enabled && v6.enabled; authorization_backend = new AuthzBackend(v6.authorization_backend); description = "Migrated from v6"; } @@ -471,8 +499,6 @@ public AuthzDomain(ConfigV6.AuthzDomain v6) { public String toString() { return "AuthzDomain [http_enabled=" + http_enabled - + ", transport_enabled=" - + transport_enabled + ", authorization_backend=" + authorization_backend + ", description=" @@ -480,6 +506,26 @@ public String toString() { + "]"; } + @JsonAnySetter + public void unknownPropertiesHandler(String name, Object value) throws JsonMappingException { + switch (name) { + case "transport_enabled": + DeprecatedSettings.logCustomDeprecationMessage( + String.format("In AuthzDomain, using authorization_backend=%s", authorization_backend), + name + ); + break; + default: + throw new UnrecognizedPropertyException( + null, + "Unrecognized field " + name + " present in the input data for AuthzDomain config", + null, + AuthzDomain.class, + name, + null + ); + } + } } public static class OnBehalfOfSettings { diff --git a/src/main/java/org/opensearch/security/setting/DeprecatedSettings.java b/src/main/java/org/opensearch/security/setting/DeprecatedSettings.java index b415dc7c7f..91eb96abdd 100644 --- a/src/main/java/org/opensearch/security/setting/DeprecatedSettings.java +++ b/src/main/java/org/opensearch/security/setting/DeprecatedSettings.java @@ -5,6 +5,7 @@ package org.opensearch.security.setting; +import org.opensearch.Version; import org.opensearch.common.logging.DeprecationLogger; import org.opensearch.common.settings.Settings; @@ -28,4 +29,18 @@ public static void checkForDeprecatedSetting(final Settings settings, final Stri ); } } + + /** + * Logs that a specific setting is deprecated, including a specific supplemental message parameter containing information that details where this setting can be removed from. Should be used in cases where a setting is not supported by the codebase and processing it would introduce errors on setup. + */ + public static void logCustomDeprecationMessage(final String deprecationLocationInformation, final String deprecatedSettingKey) { + DEPRECATION_LOGGER.deprecate( + deprecatedSettingKey, + "In OpenSearch " + + Version.CURRENT + + " the setting '{}' is deprecated, it should be removed from the relevant config file using the following location information: " + + deprecationLocationInformation, + deprecatedSettingKey + ); + } } diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java new file mode 100644 index 0000000000..5351eea57e --- /dev/null +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecureSettingsFactory.java @@ -0,0 +1,135 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.ssl; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.opensearch.common.settings.Settings; +import org.opensearch.http.HttpServerTransport; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureSettingsFactory; +import org.opensearch.plugins.SecureTransportSettingsProvider; +import org.opensearch.plugins.TransportExceptionHandler; +import org.opensearch.security.filter.SecurityRestFilter; +import org.opensearch.security.ssl.http.netty.Netty4ConditionalDecompressor; +import org.opensearch.security.ssl.http.netty.Netty4HttpRequestHeaderVerifier; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.Transport; +import org.opensearch.transport.TransportAdapterProvider; + +import io.netty.channel.ChannelInboundHandlerAdapter; + +public class OpenSearchSecureSettingsFactory implements SecureSettingsFactory { + private final ThreadPool threadPool; + private final SecurityKeyStore sks; + private final SslExceptionHandler sslExceptionHandler; + private final SecurityRestFilter restFilter; + + public OpenSearchSecureSettingsFactory( + ThreadPool threadPool, + SecurityKeyStore sks, + SslExceptionHandler sslExceptionHandler, + SecurityRestFilter restFilter + ) { + this.threadPool = threadPool; + this.sks = sks; + this.sslExceptionHandler = sslExceptionHandler; + this.restFilter = restFilter; + } + + @Override + public Optional getSecureTransportSettingsProvider(Settings settings) { + return Optional.of(new SecureTransportSettingsProvider() { + @Override + public Optional buildServerTransportExceptionHandler(Settings settings, Transport transport) { + return Optional.of(new TransportExceptionHandler() { + @Override + public void onError(Throwable t) { + sslExceptionHandler.logError(t, false); + } + }); + } + + @Override + public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { + return Optional.of(sks.createServerTransportSSLEngine()); + } + + @Override + public Optional buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException { + return Optional.of(sks.createClientTransportSSLEngine(hostname, port)); + } + }); + } + + @Override + public Optional getSecureHttpTransportSettingsProvider(Settings settings) { + return Optional.of(new SecureHttpTransportSettingsProvider() { + @Override + public Collection> getHttpTransportAdapterProviders(Settings settings) { + return List.of(new TransportAdapterProvider() { + @Override + public String name() { + return SecureNetty4HttpServerTransport.REQUEST_DECOMPRESSOR; + } + + @SuppressWarnings("unchecked") + @Override + public Optional create(Settings settings, HttpServerTransport transport, Class adapterClass) { + if (transport instanceof SecureNetty4HttpServerTransport + && ChannelInboundHandlerAdapter.class.isAssignableFrom(adapterClass)) { + return Optional.of((C) new Netty4ConditionalDecompressor()); + } else { + return Optional.empty(); + } + } + }, new TransportAdapterProvider() { + @Override + public String name() { + return SecureNetty4HttpServerTransport.REQUEST_HEADER_VERIFIER; + } + + @SuppressWarnings("unchecked") + @Override + public Optional create(Settings settings, HttpServerTransport transport, Class adapterClass) { + if (transport instanceof SecureNetty4HttpServerTransport + && ChannelInboundHandlerAdapter.class.isAssignableFrom(adapterClass)) { + return Optional.of((C) new Netty4HttpRequestHeaderVerifier(restFilter, threadPool, settings)); + } else { + return Optional.empty(); + } + } + }); + } + + @Override + public Optional buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) { + return Optional.of(new TransportExceptionHandler() { + @Override + public void onError(Throwable t) { + sslExceptionHandler.logError(t, true); + } + }); + } + + @Override + public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { + return Optional.of(sks.createHTTPSSLEngine()); + } + }); + } +} diff --git a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java index e6e4e85b33..e6a1b47888 100644 --- a/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java +++ b/src/main/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPlugin.java @@ -27,6 +27,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.function.Function; import java.util.function.Supplier; @@ -60,8 +61,12 @@ import org.opensearch.env.NodeEnvironment; import org.opensearch.http.HttpServerTransport; import org.opensearch.http.HttpServerTransport.Dispatcher; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; import org.opensearch.plugins.NetworkPlugin; import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureSettingsFactory; +import org.opensearch.plugins.SecureTransportSettingsProvider; import org.opensearch.plugins.SystemIndexPlugin; import org.opensearch.repositories.RepositoriesService; import org.opensearch.rest.RestController; @@ -70,20 +75,20 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.NonValidatingObjectMapper; import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport; import org.opensearch.security.ssl.http.netty.ValidatingDispatcher; import org.opensearch.security.ssl.rest.SecuritySSLInfoAction; import org.opensearch.security.ssl.transport.DefaultPrincipalExtractor; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.transport.SSLConfig; -import org.opensearch.security.ssl.transport.SecuritySSLNettyTransport; import org.opensearch.security.ssl.transport.SecuritySSLTransportInterceptor; import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.SecuritySettings; import org.opensearch.telemetry.tracing.Tracer; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.SharedGroupFactory; import org.opensearch.transport.Transport; import org.opensearch.transport.TransportInterceptor; +import org.opensearch.transport.netty4.ssl.SecureNetty4Transport; import org.opensearch.watcher.ResourceWatcherService; import io.netty.handler.ssl.OpenSsl; @@ -91,6 +96,21 @@ //For ES5 this class has only effect when SSL only plugin is installed public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPlugin, NetworkPlugin { + private static final Setting SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION = Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, + true, + Property.NodeScope, + Property.Filtered, + Property.Deprecated + ); + + private static final Setting SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME = Setting.boolSetting( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, + true, + Property.NodeScope, + Property.Filtered, + Property.Deprecated + ); private static boolean USE_NETTY_DEFAULT_ALLOCATOR = Booleans.parseBoolean( System.getProperty("opensearch.unsafe.use_netty_default_allocator"), @@ -112,10 +132,7 @@ public class OpenSearchSecuritySSLPlugin extends Plugin implements SystemIndexPl private final static SslExceptionHandler NOOP_SSL_EXCEPTION_HANDLER = new SslExceptionHandler() { }; protected final SSLConfig SSLConfig; - - // public OpenSearchSecuritySSLPlugin(final Settings settings, final Path configPath) { - // this(settings, configPath, false); - // } + protected volatile ThreadPool threadPool; @SuppressWarnings("removal") protected OpenSearchSecuritySSLPlugin(final Settings settings, final Path configPath, boolean disabled) { @@ -237,7 +254,7 @@ public Object run() { } @Override - public Map> getHttpTransports( + public Map> getSecureHttpTransports( Settings settings, ThreadPool threadPool, BigArrays bigArrays, @@ -247,6 +264,7 @@ public Map> getHttpTransports( NetworkService networkService, Dispatcher dispatcher, ClusterSettings clusterSettings, + SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider, Tracer tracer ) { @@ -259,19 +277,17 @@ public Map> getHttpTransports( configPath, NOOP_SSL_EXCEPTION_HANDLER ); - final SecuritySSLNettyHttpServerTransport sgsnht = new SecuritySSLNettyHttpServerTransport( - settings, + final SecureNetty4HttpServerTransport sgsnht = new SecureNetty4HttpServerTransport( + migrateSettings(settings), networkService, bigArrays, threadPool, - sks, xContentRegistry, validatingDispatcher, - NOOP_SSL_EXCEPTION_HANDLER, clusterSettings, sharedGroupFactory, - tracer, - securityRestHandler + secureHttpTransportSettingsProvider, + tracer ); return Collections.singletonMap("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport", () -> sgsnht); @@ -313,13 +329,14 @@ public List getTransportInterceptors(NamedWriteableRegistr } @Override - public Map> getTransports( + public Map> getSecureTransports( Settings settings, ThreadPool threadPool, PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService, + SecureTransportSettingsProvider secureTransportSettingsProvider, Tracer tracer ) { @@ -327,18 +344,16 @@ public Map> getTransports( if (transportSSLEnabled) { transports.put( "org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport", - () -> new SecuritySSLNettyTransport( - settings, + () -> new SecureNetty4Transport( + migrateSettings(settings), Version.CURRENT, threadPool, networkService, pageCacheRecycler, namedWriteableRegistry, circuitBreakerService, - sks, - NOOP_SSL_EXCEPTION_HANDLER, sharedGroupFactory, - SSLConfig, + secureTransportSettingsProvider, tracer ) ); @@ -363,6 +378,7 @@ public Collection createComponents( Supplier repositoriesServiceSupplier ) { + this.threadPool = threadPool; final List components = new ArrayList<>(1); if (client) { @@ -436,22 +452,13 @@ public List> getSettings() { Property.Filtered ) ); - settings.add( - Setting.boolSetting( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, - true, - Property.NodeScope, - Property.Filtered - ) - ); - settings.add( - Setting.boolSetting( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, - true, - Property.NodeScope, - Property.Filtered - ) - ); + if (!settings.stream().anyMatch(s -> s.getKey().equalsIgnoreCase(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY))) { + settings.add(SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION); + } + if (!settings.stream() + .anyMatch(s -> s.getKey().equalsIgnoreCase(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY))) { + settings.add(SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME); + } settings.add( Setting.simpleString(SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, Property.NodeScope, Property.Filtered) ); @@ -664,4 +671,63 @@ public List getSettingsFilter() { settingsFilter.add("plugins.security.*"); return settingsFilter; } + + @Override + public Optional getSecureSettingFactory(Settings settings) { + return Optional.of(new OpenSearchSecureSettingsFactory(threadPool, sks, NOOP_SSL_EXCEPTION_HANDLER, securityRestHandler)); + } + + protected Settings migrateSettings(Settings settings) { + final Settings.Builder builder = Settings.builder().put(settings); + + if (!NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED.exists(settings)) { + builder.put(NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY, SecuritySettings.SSL_DUAL_MODE_SETTING.get(settings)); + } else { + if (SecuritySettings.SSL_DUAL_MODE_SETTING.exists(settings)) { + throw new OpenSearchException( + "Only one of the settings [" + + NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY + + ", " + + SecuritySettings.SSL_DUAL_MODE_SETTING.getKey() + + " (deprecated)] could be specified but not both" + ); + } + } + + if (!NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.exists(settings)) { + builder.put( + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY, + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.get(settings) + ); + } else { + if (SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.exists(settings)) { + throw new OpenSearchException( + "Only one of the settings [" + + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY + + ", " + + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME.getKey() + + " (deprecated)] could be specified but not both" + ); + } + } + + if (!NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION.exists(settings)) { + builder.put( + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION.get(settings) + ); + } else { + if (SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION.exists(settings)) { + throw new OpenSearchException( + "Only one of the settings [" + + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY + + ", " + + SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION.getKey() + + " (deprecated)] could be specified but not both" + ); + } + } + + return builder.build(); + } } diff --git a/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java b/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java index 03b5df2100..29083d6d6b 100644 --- a/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java +++ b/src/main/java/org/opensearch/security/ssl/SecurityKeyStore.java @@ -23,25 +23,25 @@ public interface SecurityKeyStore { - public SSLEngine createHTTPSSLEngine() throws SSLException; + SSLEngine createHTTPSSLEngine() throws SSLException; - public SSLEngine createServerTransportSSLEngine() throws SSLException; + SSLEngine createServerTransportSSLEngine() throws SSLException; - public SSLEngine createClientTransportSSLEngine(String peerHost, int peerPort) throws SSLException; + SSLEngine createClientTransportSSLEngine(String peerHost, int peerPort) throws SSLException; - public String getHTTPProviderName(); + String getHTTPProviderName(); - public String getTransportServerProviderName(); + String getTransportServerProviderName(); - public String getTransportClientProviderName(); + String getTransportClientProviderName(); - public String getSubjectAlternativeNames(X509Certificate cert); + String getSubjectAlternativeNames(X509Certificate cert); - public void initHttpSSLConfig(); + void initHttpSSLConfig(); - public void initTransportSSLConfig(); + void initTransportSSLConfig(); - public X509Certificate[] getTransportCerts(); + X509Certificate[] getTransportCerts(); - public X509Certificate[] getHttpCerts(); + X509Certificate[] getHttpCerts(); } diff --git a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java index f133d997f9..b36a12da48 100644 --- a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java +++ b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4ConditionalDecompressor.java @@ -13,15 +13,12 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.codec.http.HttpContentDecompressor; -import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE; -import static org.opensearch.security.http.SecurityHttpServerTransport.SHOULD_DECOMPRESS; - public class Netty4ConditionalDecompressor extends HttpContentDecompressor { @Override protected EmbeddedChannel newContentDecoder(String contentEncoding) throws Exception { - final boolean hasAnEarlyReponse = NettyAttribute.peekFrom(ctx, EARLY_RESPONSE).isPresent(); - final boolean shouldDecompress = NettyAttribute.popFrom(ctx, SHOULD_DECOMPRESS).orElse(false); + final boolean hasAnEarlyReponse = NettyAttribute.peekFrom(ctx, Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE).isPresent(); + final boolean shouldDecompress = NettyAttribute.popFrom(ctx, Netty4HttpRequestHeaderVerifier.SHOULD_DECOMPRESS).orElse(false); if (hasAnEarlyReponse || !shouldDecompress) { // If there was an error prompting an early response,... don't decompress // If there is no explicit decompress flag,... don't decompress diff --git a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java index 9adca0f377..9afd6b0e22 100644 --- a/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java +++ b/src/main/java/org/opensearch/security/ssl/http/netty/Netty4HttpRequestHeaderVerifier.java @@ -8,6 +8,8 @@ package org.opensearch.security.ssl.http.netty; +import java.util.Set; + import org.opensearch.ExceptionsHelper; import org.opensearch.OpenSearchSecurityException; import org.opensearch.common.settings.Settings; @@ -30,15 +32,19 @@ import io.netty.channel.SimpleChannelInboundHandler; import io.netty.handler.codec.http.DefaultHttpRequest; import io.netty.handler.codec.http.HttpRequest; +import io.netty.util.AttributeKey; import io.netty.util.ReferenceCountUtil; -import static org.opensearch.security.http.SecurityHttpServerTransport.CONTEXT_TO_RESTORE; -import static org.opensearch.security.http.SecurityHttpServerTransport.EARLY_RESPONSE; -import static org.opensearch.security.http.SecurityHttpServerTransport.IS_AUTHENTICATED; -import static org.opensearch.security.http.SecurityHttpServerTransport.SHOULD_DECOMPRESS; - @Sharable public class Netty4HttpRequestHeaderVerifier extends SimpleChannelInboundHandler { + public static final AttributeKey IS_AUTHENTICATED = AttributeKey.newInstance("opensearch-http-is-authenticated"); + public static final AttributeKey SHOULD_DECOMPRESS = AttributeKey.newInstance("opensearch-http-should-decompress"); + public static final AttributeKey CONTEXT_TO_RESTORE = AttributeKey.newInstance( + "opensearch-http-request-thread-context" + ); + public static final AttributeKey> UNCONSUMED_PARAMS = AttributeKey.newInstance("opensearch-http-request-consumed-params"); + public static final AttributeKey EARLY_RESPONSE = AttributeKey.newInstance("opensearch-http-early-response"); + private final SecurityRestFilter restFilter; private final ThreadPool threadPool; private final SSLConfig sslConfig; @@ -71,8 +77,8 @@ public void channelRead0(ChannelHandlerContext ctx, DefaultHttpRequest msg) thro } // Start by setting this value to false, only requests that meet all the criteria will be decompressed - ctx.channel().attr(SHOULD_DECOMPRESS).set(Boolean.FALSE); - ctx.channel().attr(IS_AUTHENTICATED).set(Boolean.FALSE); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.SHOULD_DECOMPRESS).set(Boolean.FALSE); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED).set(Boolean.FALSE); final Netty4HttpChannel httpChannel = ctx.channel().attr(Netty4HttpServerTransport.HTTP_CHANNEL_KEY).get(); @@ -84,22 +90,25 @@ public void channelRead0(ChannelHandlerContext ctx, DefaultHttpRequest msg) thro // If request channel is completed and a response is sent, then there was a failure during authentication restFilter.checkAndAuthenticateRequest(requestChannel); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.UNCONSUMED_PARAMS).set(requestChannel.getUnconsumedParams()); + ThreadContext.StoredContext contextToRestore = threadPool.getThreadContext().newStoredContext(false); - ctx.channel().attr(CONTEXT_TO_RESTORE).set(contextToRestore); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.CONTEXT_TO_RESTORE).set(contextToRestore); - requestChannel.getQueuedResponse().ifPresent(response -> ctx.channel().attr(EARLY_RESPONSE).set(response)); + requestChannel.getQueuedResponse() + .ifPresent(response -> ctx.channel().attr(Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE).set(response)); boolean shouldSkipAuthentication = SecurityRestUtils.shouldSkipAuthentication(requestChannel); boolean shouldDecompress = !shouldSkipAuthentication && requestChannel.getQueuedResponse().isEmpty(); if (requestChannel.getQueuedResponse().isEmpty() || shouldSkipAuthentication) { // Only allow decompression on authenticated requests that also aren't one of those ^ - ctx.channel().attr(SHOULD_DECOMPRESS).set(Boolean.valueOf(shouldDecompress)); - ctx.channel().attr(IS_AUTHENTICATED).set(Boolean.TRUE); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.SHOULD_DECOMPRESS).set(Boolean.valueOf(shouldDecompress)); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.IS_AUTHENTICATED).set(Boolean.TRUE); } } catch (final OpenSearchSecurityException e) { final SecurityResponse earlyResponse = new SecurityResponse(ExceptionsHelper.status(e).getStatus(), e); - ctx.channel().attr(EARLY_RESPONSE).set(earlyResponse); + ctx.channel().attr(Netty4HttpRequestHeaderVerifier.EARLY_RESPONSE).set(earlyResponse); } catch (final SecurityRequestChannelUnsupported srcu) { // Use defaults for unsupported channels } finally { diff --git a/src/main/java/org/opensearch/security/ssl/http/netty/SecuritySSLNettyHttpServerTransport.java b/src/main/java/org/opensearch/security/ssl/http/netty/SecuritySSLNettyHttpServerTransport.java deleted file mode 100644 index fc2f31b2b0..0000000000 --- a/src/main/java/org/opensearch/security/ssl/http/netty/SecuritySSLNettyHttpServerTransport.java +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Copyright 2015-2017 floragunn GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -package org.opensearch.security.ssl.http.netty; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.ClusterSettings; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.BigArrays; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.http.HttpChannel; -import org.opensearch.http.HttpHandlingSettings; -import org.opensearch.http.netty4.Netty4HttpChannel; -import org.opensearch.http.netty4.Netty4HttpServerTransport; -import org.opensearch.security.filter.SecurityRestFilter; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.SslExceptionHandler; -import org.opensearch.telemetry.tracing.Tracer; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.SharedGroupFactory; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.ssl.ApplicationProtocolNames; -import io.netty.handler.ssl.ApplicationProtocolNegotiationHandler; -import io.netty.handler.ssl.SslHandler; - -public class SecuritySSLNettyHttpServerTransport extends Netty4HttpServerTransport { - private static final Logger logger = LogManager.getLogger(SecuritySSLNettyHttpServerTransport.class); - private final SecurityKeyStore sks; - private final SslExceptionHandler errorHandler; - private final ChannelInboundHandlerAdapter headerVerifier; - - public SecuritySSLNettyHttpServerTransport( - final Settings settings, - final NetworkService networkService, - final BigArrays bigArrays, - final ThreadPool threadPool, - final SecurityKeyStore sks, - final NamedXContentRegistry namedXContentRegistry, - final ValidatingDispatcher dispatcher, - final SslExceptionHandler errorHandler, - ClusterSettings clusterSettings, - SharedGroupFactory sharedGroupFactory, - Tracer tracer, - SecurityRestFilter restFilter - ) { - super( - settings, - networkService, - bigArrays, - threadPool, - namedXContentRegistry, - dispatcher, - clusterSettings, - sharedGroupFactory, - tracer - ); - this.sks = sks; - this.errorHandler = errorHandler; - headerVerifier = new Netty4HttpRequestHeaderVerifier(restFilter, threadPool, settings); - } - - @Override - public ChannelHandler configureServerChannelHandler() { - return new SSLHttpChannelHandler(this, handlingSettings, sks); - } - - @Override - public void onException(HttpChannel channel, Exception cause0) { - Throwable cause = cause0; - - if (cause0 instanceof DecoderException && cause0 != null) { - cause = cause0.getCause(); - } - - errorHandler.logError(cause, true); - logger.error("Exception during establishing a SSL connection: " + cause, cause); - - super.onException(channel, cause0); - } - - protected class SSLHttpChannelHandler extends Netty4HttpServerTransport.HttpChannelHandler { - /** - * Application negotiation handler to select either HTTP 1.1 or HTTP 2 protocol, based - * on client/server ALPN negotiations. - */ - private class Http2OrHttpHandler extends ApplicationProtocolNegotiationHandler { - protected Http2OrHttpHandler() { - super(ApplicationProtocolNames.HTTP_1_1); - } - - @Override - protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { - if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { - configureDefaultHttp2Pipeline(ctx.pipeline()); - } else if (ApplicationProtocolNames.HTTP_1_1.equals(protocol)) { - configureDefaultHttpPipeline(ctx.pipeline()); - } else { - throw new IllegalStateException("Unknown application protocol: " + protocol); - } - } - - @Override - public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - super.exceptionCaught(ctx, cause); - Netty4HttpChannel channel = ctx.channel().attr(HTTP_CHANNEL_KEY).get(); - if (channel != null) { - if (cause instanceof Error) { - onException(channel, new Exception(cause)); - } else { - onException(channel, (Exception) cause); - } - } - } - } - - protected SSLHttpChannelHandler( - Netty4HttpServerTransport transport, - final HttpHandlingSettings handlingSettings, - final SecurityKeyStore odsks - ) { - super(transport, handlingSettings); - } - - @Override - protected void initChannel(Channel ch) throws Exception { - super.initChannel(ch); - final SslHandler sslHandler = new SslHandler(SecuritySSLNettyHttpServerTransport.this.sks.createHTTPSSLEngine()); - ch.pipeline().addFirst("ssl_http", sslHandler); - } - - @Override - protected void configurePipeline(Channel ch) { - ch.pipeline().addLast(new Http2OrHttpHandler()); - } - } - - @Override - protected ChannelInboundHandlerAdapter createHeaderVerifier() { - return headerVerifier; - } - - @Override - protected ChannelInboundHandlerAdapter createDecompressor() { - return new Netty4ConditionalDecompressor(); - } -} diff --git a/src/main/java/org/opensearch/security/ssl/transport/DualModeSSLHandler.java b/src/main/java/org/opensearch/security/ssl/transport/DualModeSSLHandler.java deleted file mode 100644 index a7961f864b..0000000000 --- a/src/main/java/org/opensearch/security/ssl/transport/DualModeSSLHandler.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ -package org.opensearch.security.ssl.transport; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import javax.net.ssl.SSLException; - -import com.google.common.annotations.VisibleForTesting; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConnectionTestUtil; -import org.opensearch.security.ssl.util.TLSUtil; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelFutureListener; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.codec.ByteToMessageDecoder; -import io.netty.handler.ssl.SslHandler; - -/** - * Modifies the current pipeline dynamically to enable TLS - */ -public class DualModeSSLHandler extends ByteToMessageDecoder { - - private static final Logger logger = LogManager.getLogger(DualModeSSLHandler.class); - private final SecurityKeyStore securityKeyStore; - - private final SslHandler providedSSLHandler; - - public DualModeSSLHandler(SecurityKeyStore securityKeyStore) { - this(securityKeyStore, null); - } - - @VisibleForTesting - protected DualModeSSLHandler(SecurityKeyStore securityKeyStore, SslHandler providedSSLHandler) { - this.securityKeyStore = securityKeyStore; - this.providedSSLHandler = providedSSLHandler; - } - - @Override - protected void decode(ChannelHandlerContext ctx, ByteBuf in, List out) throws Exception { - // Will use the first six bytes to detect a protocol. - if (in.readableBytes() < 6) { - return; - } - int offset = in.readerIndex(); - if (in.getCharSequence(offset, 6, StandardCharsets.UTF_8).equals(SSLConnectionTestUtil.DUAL_MODE_CLIENT_HELLO_MSG)) { - logger.debug("Received DualSSL Client Hello message"); - ByteBuf responseBuffer = Unpooled.buffer(6); - responseBuffer.writeCharSequence(SSLConnectionTestUtil.DUAL_MODE_SERVER_HELLO_MSG, StandardCharsets.UTF_8); - ctx.writeAndFlush(responseBuffer).addListener(ChannelFutureListener.CLOSE); - return; - } - - if (TLSUtil.isTLS(in)) { - logger.debug("Identified request as SSL request"); - enableSsl(ctx); - } else { - logger.debug("Identified request as non SSL request, running in HTTP mode as dual mode is enabled"); - ctx.pipeline().remove(this); - } - } - - private void enableSsl(ChannelHandlerContext ctx) throws SSLException { - SslHandler sslHandler; - if (providedSSLHandler != null) { - sslHandler = providedSSLHandler; - } else { - sslHandler = new SslHandler(securityKeyStore.createServerTransportSSLEngine()); - } - ChannelPipeline p = ctx.pipeline(); - p.addAfter("port_unification_handler", "ssl_server", sslHandler); - p.remove(this); - logger.debug("Removed port unification handler and added SSL handler as incoming request is SSL"); - } -} diff --git a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java deleted file mode 100644 index 242c7c56ed..0000000000 --- a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLNettyTransport.java +++ /dev/null @@ -1,299 +0,0 @@ -/* - * Copyright 2015-2017 floragunn GmbH - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.ssl.transport; - -import java.net.InetSocketAddress; -import java.net.SocketAddress; -import java.security.AccessController; -import java.security.PrivilegedAction; -import javax.net.ssl.SSLEngine; -import javax.net.ssl.SSLException; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.ExceptionsHelper; -import org.opensearch.Version; -import org.opensearch.cluster.node.DiscoveryNode; -import org.opensearch.common.network.NetworkService; -import org.opensearch.common.settings.Settings; -import org.opensearch.common.util.PageCacheRecycler; -import org.opensearch.core.common.io.stream.NamedWriteableRegistry; -import org.opensearch.core.indices.breaker.CircuitBreakerService; -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.SslExceptionHandler; -import org.opensearch.security.ssl.util.SSLConfigConstants; -import org.opensearch.security.ssl.util.SSLConnectionTestResult; -import org.opensearch.security.ssl.util.SSLConnectionTestUtil; -import org.opensearch.telemetry.tracing.Tracer; -import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.SharedGroupFactory; -import org.opensearch.transport.TcpChannel; -import org.opensearch.transport.netty4.Netty4Transport; - -import io.netty.channel.Channel; -import io.netty.channel.ChannelHandler; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelOutboundHandlerAdapter; -import io.netty.channel.ChannelPromise; -import io.netty.handler.codec.DecoderException; -import io.netty.handler.ssl.SslHandler; - -public class SecuritySSLNettyTransport extends Netty4Transport { - - private static final Logger logger = LogManager.getLogger(SecuritySSLNettyTransport.class); - private final SecurityKeyStore ossks; - private final SslExceptionHandler errorHandler; - private final SSLConfig SSLConfig; - - public SecuritySSLNettyTransport( - final Settings settings, - final Version version, - final ThreadPool threadPool, - final NetworkService networkService, - final PageCacheRecycler pageCacheRecycler, - final NamedWriteableRegistry namedWriteableRegistry, - final CircuitBreakerService circuitBreakerService, - final SecurityKeyStore ossks, - final SslExceptionHandler errorHandler, - SharedGroupFactory sharedGroupFactory, - final SSLConfig SSLConfig, - final Tracer tracer - ) { - super( - settings, - version, - threadPool, - networkService, - pageCacheRecycler, - namedWriteableRegistry, - circuitBreakerService, - sharedGroupFactory, - tracer - ); - - this.ossks = ossks; - this.errorHandler = errorHandler; - this.SSLConfig = SSLConfig; - } - - @Override - public void onException(TcpChannel channel, Exception e) { - - Throwable cause = e; - - if (e instanceof DecoderException && e != null) { - cause = e.getCause(); - } - - errorHandler.logError(cause, false); - logger.error("Exception during establishing a SSL connection: " + cause, cause); - - super.onException(channel, e); - } - - @Override - protected ChannelHandler getServerChannelInitializer(String name) { - return new SSLServerChannelInitializer(name); - } - - @Override - protected ChannelHandler getClientChannelInitializer(DiscoveryNode node) { - return new SSLClientChannelInitializer(node); - } - - protected class SSLServerChannelInitializer extends Netty4Transport.ServerChannelInitializer { - - public SSLServerChannelInitializer(String name) { - super(name); - } - - @Override - protected void initChannel(Channel ch) throws Exception { - super.initChannel(ch); - - boolean dualModeEnabled = SSLConfig.isDualModeEnabled(); - if (dualModeEnabled) { - logger.info("SSL Dual mode enabled, using port unification handler"); - final ChannelHandler portUnificationHandler = new DualModeSSLHandler(ossks); - ch.pipeline().addFirst("port_unification_handler", portUnificationHandler); - } else { - final SslHandler sslHandler = new SslHandler(ossks.createServerTransportSSLEngine()); - ch.pipeline().addFirst("ssl_server", sslHandler); - } - } - - @Override - public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (cause instanceof DecoderException && cause != null) { - cause = cause.getCause(); - } - - errorHandler.logError(cause, false); - logger.error("Exception during establishing a SSL connection: " + cause, cause); - - super.exceptionCaught(ctx, cause); - } - } - - protected static class ClientSSLHandler extends ChannelOutboundHandlerAdapter { - private final Logger log = LogManager.getLogger(this.getClass()); - private final SecurityKeyStore sks; - private final boolean hostnameVerificationEnabled; - private final boolean hostnameVerificationResovleHostName; - private final SslExceptionHandler errorHandler; - - private ClientSSLHandler( - final SecurityKeyStore sks, - final boolean hostnameVerificationEnabled, - final boolean hostnameVerificationResovleHostName, - final SslExceptionHandler errorHandler - ) { - this.sks = sks; - this.hostnameVerificationEnabled = hostnameVerificationEnabled; - this.hostnameVerificationResovleHostName = hostnameVerificationResovleHostName; - this.errorHandler = errorHandler; - } - - @Override - public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (cause instanceof DecoderException && cause != null) { - cause = cause.getCause(); - } - - errorHandler.logError(cause, false); - logger.error("Exception during establishing a SSL connection: " + cause, cause); - - super.exceptionCaught(ctx, cause); - } - - @Override - public void connect(ChannelHandlerContext ctx, SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) - throws Exception { - SSLEngine engine = null; - try { - if (hostnameVerificationEnabled) { - final InetSocketAddress inetSocketAddress = (InetSocketAddress) remoteAddress; - String hostname = null; - if (hostnameVerificationResovleHostName) { - hostname = inetSocketAddress.getHostName(); - } else { - hostname = inetSocketAddress.getHostString(); - } - - if (log.isDebugEnabled()) { - log.debug( - "Hostname of peer is {} ({}/{}) with hostnameVerificationResovleHostName: {}", - hostname, - inetSocketAddress.getHostName(), - inetSocketAddress.getHostString(), - hostnameVerificationResovleHostName - ); - } - - engine = sks.createClientTransportSSLEngine(hostname, inetSocketAddress.getPort()); - } else { - engine = sks.createClientTransportSSLEngine(null, -1); - } - } catch (final SSLException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - final SslHandler sslHandler = new SslHandler(engine); - ctx.pipeline().replace(this, "ssl_client", sslHandler); - super.connect(ctx, remoteAddress, localAddress, promise); - } - } - - protected class SSLClientChannelInitializer extends Netty4Transport.ClientChannelInitializer { - private final boolean hostnameVerificationEnabled; - private final boolean hostnameVerificationResovleHostName; - private final DiscoveryNode node; - private SSLConnectionTestResult connectionTestResult; - - @SuppressWarnings("removal") - public SSLClientChannelInitializer(DiscoveryNode node) { - this.node = node; - hostnameVerificationEnabled = settings.getAsBoolean( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, - true - ); - hostnameVerificationResovleHostName = settings.getAsBoolean( - SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, - true - ); - - connectionTestResult = SSLConnectionTestResult.SSL_AVAILABLE; - if (SSLConfig.isDualModeEnabled()) { - SSLConnectionTestUtil sslConnectionTestUtil = new SSLConnectionTestUtil( - node.getAddress().getAddress(), - node.getAddress().getPort() - ); - connectionTestResult = AccessController.doPrivileged( - (PrivilegedAction) sslConnectionTestUtil::testConnection - ); - } - } - - @Override - protected void initChannel(Channel ch) throws Exception { - super.initChannel(ch); - - if (connectionTestResult == SSLConnectionTestResult.OPENSEARCH_PING_FAILED) { - logger.error( - "SSL dual mode is enabled but dual mode handshake and OpenSearch ping has failed during client connection setup, closing channel" - ); - ch.close(); - return; - } - - if (connectionTestResult == SSLConnectionTestResult.SSL_AVAILABLE) { - logger.debug("Connection to {} needs to be ssl, adding ssl handler to the client channel ", node.getHostName()); - ch.pipeline() - .addFirst( - "client_ssl_handler", - new ClientSSLHandler(ossks, hostnameVerificationEnabled, hostnameVerificationResovleHostName, errorHandler) - ); - } else { - logger.debug("Connection to {} needs to be non ssl", node.getHostName()); - } - } - - @Override - public final void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { - if (cause instanceof DecoderException && cause != null) { - cause = cause.getCause(); - } - - errorHandler.logError(cause, false); - logger.error("Exception during establishing a SSL connection: " + cause, cause); - - super.exceptionCaught(ctx, cause); - } - } -} 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 78c98dd99f..7002171595 100644 --- a/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java +++ b/src/main/java/org/opensearch/security/ssl/transport/SecuritySSLRequestHandler.java @@ -21,6 +21,7 @@ import java.security.cert.Certificate; import java.security.cert.X509Certificate; import java.util.Arrays; +import java.util.Set; import javax.net.ssl.SSLPeerUnverifiedException; import org.apache.logging.log4j.LogManager; @@ -33,15 +34,12 @@ import org.opensearch.security.ssl.util.ExceptionUtils; import org.opensearch.security.ssl.util.SSLRequestHelper; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SerializationFormat; import org.opensearch.tasks.Task; import org.opensearch.threadpool.ThreadPool; -import org.opensearch.transport.TaskTransportChannel; -import org.opensearch.transport.TcpChannel; -import org.opensearch.transport.TcpTransportChannel; import org.opensearch.transport.TransportChannel; import org.opensearch.transport.TransportRequest; import org.opensearch.transport.TransportRequestHandler; -import org.opensearch.transport.netty4.Netty4TcpChannel; import io.netty.handler.ssl.SslHandler; @@ -55,6 +53,8 @@ public class SecuritySSLRequestHandler implements Tr private final SslExceptionHandler errorHandler; private final SSLConfig SSLConfig; + private static final Set DEFAULT_CHANNEL_TYPES = Set.of("direct", "transport"); + public SecuritySSLRequestHandler( String action, TransportRequestHandler actualHandler, @@ -86,9 +86,14 @@ public final void messageReceived(T request, TransportChannel channel, Task task ThreadContext threadContext = getThreadContext(); + String channelType = channel.getChannelType(); + if (!DEFAULT_CHANNEL_TYPES.contains(channelType)) { + channel = getInnerChannel(channel); + } + threadContext.putTransient( ConfigConstants.USE_JDK_SERIALIZATION, - channel.getVersion().before(ConfigConstants.FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION) + SerializationFormat.determineFormat(channel.getVersion()) == SerializationFormat.JDK ); if (SSLRequestHelper.containsBadHeader(threadContext, "_opendistro_security_ssl_")) { @@ -97,32 +102,13 @@ public final void messageReceived(T request, TransportChannel channel, Task task throw exception; } - String channelType = channel.getChannelType(); - if (!channelType.equals("direct") && !channelType.equals("transport")) { - channel = getInnerChannel(channel); - } - if (!"transport".equals(channel.getChannelType())) { // netty4 messageReceivedDecorate(request, actualHandler, channel, task); return; } try { - - Netty4TcpChannel nettyChannel = null; - - if (channel instanceof TaskTransportChannel) { - final TransportChannel inner = ((TaskTransportChannel) channel).getChannel(); - nettyChannel = (Netty4TcpChannel) ((TcpTransportChannel) inner).getChannel(); - } else if (channel instanceof TcpTransportChannel) { - final TcpChannel inner = ((TcpTransportChannel) channel).getChannel(); - nettyChannel = (Netty4TcpChannel) inner; - } else { - throw new Exception("Invalid channel of type " + channel.getClass() + " (" + channel.getChannelType() + ")"); - } - - final SslHandler sslhandler = (SslHandler) nettyChannel.getNettyChannel().pipeline().get("ssl_server"); - + final SslHandler sslhandler = channel.get("ssl_server", SslHandler.class).orElse(null); if (sslhandler == null) { if (SSLConfig.isDualModeEnabled()) { log.info("Communication in dual mode. Skipping SSL handler check"); diff --git a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java index 2449146b39..a3b9348496 100644 --- a/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java +++ b/src/main/java/org/opensearch/security/ssl/util/SSLConfigConstants.java @@ -101,7 +101,7 @@ public final class SSLConfigConstants { private static final String[] _SECURE_SSL_PROTOCOLS = { "TLSv1.3", "TLSv1.2", "TLSv1.1" }; - public static final String[] getSecureSSLProtocols(Settings settings, boolean http) { + public static String[] getSecureSSLProtocols(Settings settings, boolean http) { List configuredProtocols = null; if (settings != null) { @@ -233,7 +233,7 @@ public static final String[] getSecureSSLProtocols(Settings settings, boolean ht }; // @formatter:on - public static final List getSecureSSLCiphers(Settings settings, boolean http) { + public static List getSecureSSLCiphers(Settings settings, boolean http) { List configuredCiphers = null; diff --git a/src/main/java/org/opensearch/security/state/SecurityConfig.java b/src/main/java/org/opensearch/security/state/SecurityConfig.java new file mode 100644 index 0000000000..f8de098365 --- /dev/null +++ b/src/main/java/org/opensearch/security/state/SecurityConfig.java @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.state; + +import java.io.IOException; +import java.time.Instant; +import java.util.Objects; +import java.util.Optional; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.securityconf.impl.CType; + +import static java.time.format.DateTimeFormatter.ISO_INSTANT; + +public class SecurityConfig implements Writeable, ToXContent { + + private final CType type; + + private final Instant lastModified; + + private final String hash; + + public SecurityConfig(final CType type, final String hash, final Instant lastModified) { + this.type = type; + this.hash = hash; + this.lastModified = lastModified; + } + + public SecurityConfig(final StreamInput in) throws IOException { + this.type = in.readEnum(CType.class); + this.hash = in.readString(); + this.lastModified = in.readOptionalInstant(); + } + + public Optional lastModified() { + return Optional.ofNullable(lastModified); + } + + public CType type() { + return type; + } + + public String hash() { + return hash; + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeEnum(type); + out.writeString(hash); + out.writeOptionalInstant(lastModified); + } + + @Override + public XContentBuilder toXContent(final XContentBuilder xContentBuilder, final Params params) throws IOException { + xContentBuilder.startObject(type.toLCString()).field("hash", hash); + if (lastModified != null) { + xContentBuilder.field("last_modified", ISO_INSTANT.format(lastModified)); + } else { + xContentBuilder.nullField("last_modified"); + } + return xContentBuilder.endObject(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecurityConfig that = (SecurityConfig) o; + return type == that.type && Objects.equals(lastModified, that.lastModified) && Objects.equals(hash, that.hash); + } + + @Override + public int hashCode() { + return Objects.hash(type, lastModified, hash); + } + + public final static class Builder { + + private final CType type; + + private Instant lastModified; + + private String hash; + + Builder(final SecurityConfig securityConfig) { + this.type = securityConfig.type; + this.lastModified = securityConfig.lastModified; + this.hash = securityConfig.hash; + } + + public Builder withHash(final String hash) { + this.hash = hash; + return this; + } + + public Builder withLastModified(final Instant lastModified) { + this.lastModified = lastModified; + return this; + } + + public SecurityConfig build() { + return new SecurityConfig(type, hash, lastModified); + } + + } + + public static SecurityConfig.Builder from(final SecurityConfig securityConfig) { + return new SecurityConfig.Builder(securityConfig); + } + +} diff --git a/src/main/java/org/opensearch/security/state/SecurityMetadata.java b/src/main/java/org/opensearch/security/state/SecurityMetadata.java new file mode 100644 index 0000000000..f8e2e043fd --- /dev/null +++ b/src/main/java/org/opensearch/security/state/SecurityMetadata.java @@ -0,0 +1,128 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.state; + +import java.io.IOException; +import java.time.Instant; +import java.util.Comparator; +import java.util.Objects; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.ImmutableSortedSet; + +import org.opensearch.Version; +import org.opensearch.cluster.AbstractNamedDiffable; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.NamedDiff; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; + +import static java.time.format.DateTimeFormatter.ISO_INSTANT; + +public final class SecurityMetadata extends AbstractNamedDiffable implements ClusterState.Custom { + + public final static String TYPE = "security"; + + private final Instant created; + + private final Set configuration; + + public SecurityMetadata(final Instant created, final Set configuration) { + this.created = created; + this.configuration = configuration; + } + + public SecurityMetadata(StreamInput in) throws IOException { + this.created = in.readInstant(); + this.configuration = in.readSet(SecurityConfig::new); + } + + public Instant created() { + return created; + } + + public Set configuration() { + return configuration; + } + + @Override + public Version getMinimalSupportedVersion() { + return Version.CURRENT.minimumCompatibilityVersion(); + } + + @Override + public String getWriteableName() { + return TYPE; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeInstant(created); + out.writeCollection(configuration); + } + + @Override + public XContentBuilder toXContent(XContentBuilder xContentBuilder, Params params) throws IOException { + xContentBuilder.field("created", ISO_INSTANT.format(created)); + xContentBuilder.startObject("configuration"); + for (final var securityConfig : configuration) { + securityConfig.toXContent(xContentBuilder, EMPTY_PARAMS); + } + return xContentBuilder.endObject(); + } + + public static NamedDiff readDiffFrom(StreamInput in) throws IOException { + return readDiffFrom(ClusterState.Custom.class, TYPE, in); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SecurityMetadata that = (SecurityMetadata) o; + return Objects.equals(created, that.created) && Objects.equals(configuration, that.configuration); + } + + @Override + public int hashCode() { + return Objects.hash(created, configuration); + } + + public final static class Builder { + + private final Instant created; + + private final ImmutableSet.Builder configuration = new ImmutableSortedSet.Builder<>( + Comparator.comparing(SecurityConfig::type) + ); + + Builder(SecurityMetadata oldMetadata) { + this.created = oldMetadata.created; + this.configuration.addAll(oldMetadata.configuration); + } + + public Builder withSecurityConfig(final SecurityConfig securityConfig) { + this.configuration.add(securityConfig); + return this; + } + + public SecurityMetadata build() { + return new SecurityMetadata(created, configuration.build()); + } + + } + + public static SecurityMetadata.Builder from(final SecurityMetadata securityMetadata) { + return new SecurityMetadata.Builder(securityMetadata); + } + +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 1f5728edfb..9c671a80f9 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -35,7 +35,6 @@ 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; @@ -165,6 +164,7 @@ public class ConfigConstants { ); public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS = "opendistro_security.audit.ignore_users"; public static final String OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS = "opendistro_security.audit.ignore_requests"; + public static final String SECURITY_AUDIT_IGNORE_HEADERS = "plugins.security.audit.ignore_headers"; public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS = "opendistro_security.audit.resolve_bulk_requests"; public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_VERIFY_HOSTNAMES_DEFAULT = true; public static final boolean OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT = false; @@ -219,9 +219,14 @@ public class ConfigConstants { public static final String SECURITY_NODES_DN = "plugins.security.nodes_dn"; public static final String SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED = "plugins.security.nodes_dn_dynamic_config_enabled"; public static final String SECURITY_DISABLED = "plugins.security.disabled"; + public static final String SECURITY_CACHE_TTL_MINUTES = "plugins.security.cache.ttl_minutes"; public static final String SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = "plugins.security.allow_unsafe_democertificates"; public static final String SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = "plugins.security.allow_default_init_securityindex"; + + public static final String SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE = + "plugins.security.allow_default_init_securityindex.use_cluster_state"; + public static final String SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST = "plugins.security.background_init_if_securityindex_not_exist"; @@ -280,6 +285,8 @@ public enum RolesMappingResolution { // Illegal Opcodes from here on public static final String SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY = "plugins.security.unsupported.disable_rest_auth_initially"; + public static final String SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS = + "plugins.security.unsupported.delay_initialization_seconds"; public static final String SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY = "plugins.security.unsupported.disable_intertransport_auth_initially"; public static final String SECURITY_UNSUPPORTED_PASSIVE_INTERTRANSPORT_AUTH_INITIALLY = @@ -326,7 +333,6 @@ 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_2_11_0; // On-behalf-of endpoints settings // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings @@ -334,6 +340,9 @@ public enum RolesMappingResolution { public static final boolean EXTENSIONS_BWC_PLUGIN_MODE_DEFAULT = false; // CS-ENFORCE-SINGLE + // Variable for initial admin password support + public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD"; + public static Set getSettingAsSet( final Settings settings, final String key, diff --git a/src/main/java/org/opensearch/security/support/ConfigHelper.java b/src/main/java/org/opensearch/security/support/ConfigHelper.java index 4f310f6af7..e8526478f2 100644 --- a/src/main/java/org/opensearch/security/support/ConfigHelper.java +++ b/src/main/java/org/opensearch/security/support/ConfigHelper.java @@ -57,6 +57,7 @@ import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +@Deprecated public class ConfigHelper { private static final Logger LOGGER = LogManager.getLogger(ConfigHelper.class); diff --git a/src/main/java/org/opensearch/security/support/JsonFlattener.java b/src/main/java/org/opensearch/security/support/JsonFlattener.java new file mode 100644 index 0000000000..ba2819a886 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/JsonFlattener.java @@ -0,0 +1,66 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ + +package org.opensearch.security.support; + +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; + +import org.opensearch.core.common.Strings; +import org.opensearch.security.DefaultObjectMapper; + +public class JsonFlattener { + + public static Map flattenAsMap(String jsonString) { + try { + final TypeReference> typeReference = new TypeReference<>() { + }; + final Map jsonMap = DefaultObjectMapper.objectMapper.readValue(jsonString, typeReference); + final Map flattenMap = new LinkedHashMap<>(); + flattenEntries("", jsonMap.entrySet(), flattenMap); + return flattenMap; + } catch (final IOException ioe) { + throw new IllegalArgumentException("Unparseable json", ioe); + } + } + + private static void flattenEntries(String prefix, final Iterable> entries, final Map result) { + if (!Strings.isNullOrEmpty(prefix)) { + prefix += "."; + } + + for (final Map.Entry e : entries) { + flattenElement(prefix.concat(e.getKey()), e.getValue(), result); + } + } + + @SuppressWarnings("unchecked") + private static void flattenElement(String prefix, final Object source, final Map result) { + if (source instanceof Iterable) { + flattenCollection(prefix, (Iterable) source, result); + } + if (source instanceof Map) { + flattenEntries(prefix, ((Map) source).entrySet(), result); + } + result.put(prefix, source); + } + + private static void flattenCollection(String prefix, final Iterable objects, final Map result) { + int counter = 0; + for (final Object o : objects) { + flattenElement(prefix + "[" + counter + "]", o, result); + counter++; + } + } + +} diff --git a/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java b/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java new file mode 100644 index 0000000000..1ed8a99614 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java @@ -0,0 +1,233 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.support; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSortedSet; +import com.google.common.hash.Hashing; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.DocWriteRequest; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.state.SecurityConfig; + +import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; +import static org.opensearch.security.support.YamlConfigReader.emptyJsonConfigFor; +import static org.opensearch.security.support.YamlConfigReader.yamlContentFor; + +public class SecurityIndexHandler { + + private final static int MINIMUM_HASH_BITS = 128; + + private static final Logger LOGGER = LogManager.getLogger(SecurityIndexHandler.class); + + private final Settings settings; + + private final Client client; + + private final String indexName; + + public SecurityIndexHandler(final String indexName, final Settings settings, final Client client) { + this.indexName = indexName; + this.settings = settings; + this.client = client; + } + + public final static Map INDEX_SETTINGS = Map.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + + public void createIndex(ActionListener listener) { + try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { + client.admin() + .indices() + .create( + new CreateIndexRequest(indexName).settings(INDEX_SETTINGS).waitForActiveShards(1), + ActionListener.runBefore(ActionListener.wrap(r -> { + if (r.isAcknowledged()) { + listener.onResponse(true); + } else listener.onFailure(new SecurityException("Couldn't create security index " + indexName)); + }, listener::onFailure), threadContext::restore) + ); + } + } + + public void uploadDefaultConfiguration(final Path configDir, final ActionListener> listener) { + try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { + AccessController.doPrivileged((PrivilegedAction) () -> { + try { + LOGGER.info("Uploading default security configuration from {}", configDir.toAbsolutePath()); + final var bulkRequest = new BulkRequest().setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); + final var configuration = new ImmutableSortedSet.Builder<>(Comparator.comparing(SecurityConfig::type)); + for (final var cType : CType.values()) { + final var fileExists = Files.exists(cType.configFile(configDir)); + // Audit config is not packaged by default and while list is deprecated + if ((cType == CType.AUDIT || cType == CType.WHITELIST) && !fileExists) continue; + if (cType == CType.WHITELIST) { + LOGGER.warn( + "WHITELIST configuration type is deprecated and will be replaced with ALLOWLIST in the next major version" + ); + } + final var yamlContent = yamlContentFor(cType, configDir); + final var hash = Hashing.goodFastHash(MINIMUM_HASH_BITS).hashBytes(yamlContent.toBytesRef().bytes); + configuration.add(new SecurityConfig(cType, hash.toString(), null)); + bulkRequest.add( + new IndexRequest(indexName).id(cType.toLCString()) + .opType(DocWriteRequest.OpType.INDEX) + .source(cType.toLCString(), yamlContent) + ); + } + client.bulk(bulkRequest, ActionListener.runBefore(ActionListener.wrap(r -> { + if (r.hasFailures()) { + listener.onFailure(new SecurityException(r.buildFailureMessage())); + return; + } + listener.onResponse(configuration.build()); + }, listener::onFailure), threadContext::restore)); + } catch (final IOException ioe) { + listener.onFailure(new SecurityException(ioe)); + } + return null; + }); + } + } + + public void loadConfiguration( + final Set configuration, + final ActionListener>> listener + ) { + try (final ThreadContext.StoredContext threadContext = client.threadPool().getThreadContext().stashContext()) { + client.threadPool().getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + final var configurationTypes = configuration.stream().map(SecurityConfig::type).collect(Collectors.toUnmodifiableList()); + client.multiGet(newMultiGetRequest(configurationTypes), ActionListener.runBefore(ActionListener.wrap(r -> { + final var cTypeConfigsBuilder = ImmutableMap.>builderWithExpectedSize( + configuration.size() + ); + var hasFailures = false; + for (final var item : r.getResponses()) { + if (item.isFailed()) { + listener.onFailure(new SecurityException(multiGetFailureMessage(item.getId(), item.getFailure()))); + hasFailures = true; + break; + } + final var cType = CType.fromString(item.getId()); + final var cTypeResponse = item.getResponse(); + if (cTypeResponse.isExists() && !cTypeResponse.isSourceEmpty()) { + final var config = buildDynamicConfiguration( + cType, + cTypeResponse.getSourceAsBytesRef(), + cTypeResponse.getSeqNo(), + cTypeResponse.getPrimaryTerm() + ); + if (config.getVersion() != DEFAULT_CONFIG_VERSION) { + listener.onFailure( + new SecurityException("Version " + config.getVersion() + " is not supported for " + cType.name()) + ); + hasFailures = true; + break; + } + cTypeConfigsBuilder.put(cType, config); + } else { + if (!cType.emptyIfMissing()) { + listener.onFailure(new SecurityException("Missing required configuration for type: " + cType)); + hasFailures = true; + break; + } + cTypeConfigsBuilder.put( + cType, + SecurityDynamicConfiguration.fromJson( + emptyJsonConfigFor(cType), + cType, + DEFAULT_CONFIG_VERSION, + cTypeResponse.getSeqNo(), + cTypeResponse.getPrimaryTerm() + ) + ); + } + } + if (!hasFailures) { + listener.onResponse(cTypeConfigsBuilder.build()); + } + }, listener::onFailure), threadContext::restore)); + } + } + + private MultiGetRequest newMultiGetRequest(final List configurationTypes) { + final var request = new MultiGetRequest().realtime(true).refresh(true); + for (final var cType : configurationTypes) { + request.add(indexName, cType.toLCString()); + } + return request; + } + + private SecurityDynamicConfiguration buildDynamicConfiguration( + final CType cType, + final BytesReference bytesRef, + final long seqNo, + final long primaryTerm + ) { + try { + final var source = SecurityUtils.replaceEnvVars(configTypeSource(bytesRef.streamInput()), settings); + final var jsonNode = DefaultObjectMapper.readTree(source); + var version = 1; + if (jsonNode.has("_meta")) { + if (jsonNode.get("_meta").has("config_version")) { + version = jsonNode.get("_meta").get("config_version").asInt(); + } + } + return SecurityDynamicConfiguration.fromJson(source, cType, version, seqNo, primaryTerm); + } catch (IOException e) { + throw new SecurityException("Couldn't parse content for " + cType, e); + } + } + + private String configTypeSource(final InputStream inputStream) throws IOException { + final var jsonContent = XContentType.JSON.xContent(); + try (final var parser = jsonContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, inputStream)) { + parser.nextToken(); + parser.nextToken(); + parser.nextToken(); + return new String(parser.binaryValue(), StandardCharsets.UTF_8); + } + } + + private String multiGetFailureMessage(final String cTypeId, final MultiGetResponse.Failure failure) { + return String.format("Failure %s retrieving configuration for %s (index=%s)", failure, cTypeId, indexName); + } + +} diff --git a/src/main/java/org/opensearch/security/support/SerializationFormat.java b/src/main/java/org/opensearch/security/support/SerializationFormat.java new file mode 100644 index 0000000000..210a5cf6a5 --- /dev/null +++ b/src/main/java/org/opensearch/security/support/SerializationFormat.java @@ -0,0 +1,35 @@ +/* + * 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.opensearch.Version; + +public enum SerializationFormat { + /** Uses Java's native serialization system */ + JDK, + /** Uses a custom serializer built ontop of OpenSearch 2.11 */ + CustomSerializer_2_11; + + private static final Version FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION = Version.V_2_11_0; + private static final Version CUSTOM_SERIALIZATION_NO_LONGER_SUPPORTED_OS_VERSION = Version.V_2_14_0; + + /** + * Determines the format of serialization that should be used from a version identifier + */ + public static SerializationFormat determineFormat(final Version version) { + if (version.onOrAfter(FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION) + && version.before(CUSTOM_SERIALIZATION_NO_LONGER_SUPPORTED_OS_VERSION)) { + return SerializationFormat.CustomSerializer_2_11; + } + return SerializationFormat.JDK; + } +} diff --git a/src/main/java/org/opensearch/security/support/WildcardMatcher.java b/src/main/java/org/opensearch/security/support/WildcardMatcher.java index 99c34b53ba..d811a73730 100644 --- a/src/main/java/org/opensearch/security/support/WildcardMatcher.java +++ b/src/main/java/org/opensearch/security/support/WildcardMatcher.java @@ -150,7 +150,9 @@ public String toString() { }; public static WildcardMatcher from(String pattern, boolean caseSensitive) { - if (pattern.equals("*")) { + if (pattern == null) { + return NONE; + } else if (pattern.equals("*")) { return ANY; } else if (pattern.startsWith("/") && pattern.endsWith("/")) { return new RegexMatcher(pattern, caseSensitive); @@ -168,7 +170,9 @@ public static WildcardMatcher from(String pattern) { // This may in future use more optimized techniques to combine multiple WildcardMatchers in a single automaton public static WildcardMatcher from(Stream stream, boolean caseSensitive) { Collection matchers = stream.map(t -> { - if (t instanceof String) { + if (t == null) { + return NONE; + } else if (t instanceof String) { return WildcardMatcher.from(((String) t), caseSensitive); } else if (t instanceof WildcardMatcher) { return ((WildcardMatcher) t); diff --git a/src/main/java/org/opensearch/security/support/YamlConfigReader.java b/src/main/java/org/opensearch/security/support/YamlConfigReader.java new file mode 100644 index 0000000000..237e5b5bfb --- /dev/null +++ b/src/main/java/org/opensearch/security/support/YamlConfigReader.java @@ -0,0 +1,95 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.support; + +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.Meta; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; + +/** + * Read YAML security config files + */ +public final class YamlConfigReader { + + private static final Logger LOGGER = LogManager.getLogger(YamlConfigReader.class); + + public static BytesReference yamlContentFor(final CType cType, final Path configDir) throws IOException { + final var yamlXContent = XContentType.YAML.xContent(); + try ( + final var r = newReader(cType, configDir); + final var parser = yamlXContent.createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, r) + ) { + parser.nextToken(); + try (final var xContentBuilder = XContentFactory.jsonBuilder()) { + xContentBuilder.copyCurrentStructure(parser); + final var bytesRef = BytesReference.bytes(xContentBuilder); + validateYamlContent(cType, bytesRef.streamInput()); + return bytesRef; + } + } + } + + public static Reader newReader(final CType cType, final Path configDir) throws IOException { + final var cTypeFile = cType.configFile(configDir); + final var fileExists = Files.exists(cTypeFile); + if (!fileExists && !cType.emptyIfMissing()) { + throw new IOException("Couldn't find configuration file " + cTypeFile.getFileName()); + } + if (fileExists) { + LOGGER.info("Reading {} configuration from {}", cType, cTypeFile.getFileName()); + return new FileReader(cTypeFile.toFile(), StandardCharsets.UTF_8); + } else { + LOGGER.info("Reading empty {} configuration", cType); + return new StringReader(emptyYamlConfigFor(cType)); + } + } + + private static SecurityDynamicConfiguration emptyConfigFor(final CType cType) { + final var emptyConfiguration = SecurityDynamicConfiguration.empty(); + emptyConfiguration.setCType(cType); + emptyConfiguration.set_meta(new Meta()); + emptyConfiguration.get_meta().setConfig_version(DEFAULT_CONFIG_VERSION); + emptyConfiguration.get_meta().setType(cType.toLCString()); + return emptyConfiguration; + } + + public static String emptyJsonConfigFor(final CType cType) throws IOException { + return DefaultObjectMapper.writeValueAsString(emptyConfigFor(cType), false); + } + + public static String emptyYamlConfigFor(final CType cType) throws IOException { + return DefaultObjectMapper.YAML_MAPPER.writeValueAsString(emptyConfigFor(cType)); + } + + private static void validateYamlContent(final CType cType, final InputStream in) throws IOException { + SecurityDynamicConfiguration.fromNode(DefaultObjectMapper.YAML_MAPPER.readTree(in), cType, DEFAULT_CONFIG_VERSION, -1, -1); + } + +} diff --git a/src/main/java/org/opensearch/security/tools/democonfig/CertificateGenerator.java b/src/main/java/org/opensearch/security/tools/democonfig/CertificateGenerator.java new file mode 100644 index 0000000000..077bf4610f --- /dev/null +++ b/src/main/java/org/opensearch/security/tools/democonfig/CertificateGenerator.java @@ -0,0 +1,55 @@ +/* + * 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.tools.democonfig; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +/** + * This class creates demo certificate files + */ +public class CertificateGenerator { + + private final Installer installer; + + public CertificateGenerator(Installer installer) { + this.installer = installer; + } + + /** + * Creates demo super-admin, node and root certificates by iterating through Certificates enum + */ + public void createDemoCertificates() { + for (Certificates cert : Certificates.values()) { + String filePath = this.installer.OPENSEARCH_CONF_DIR + File.separator + cert.getFileName(); + writeCertificateToFile(filePath, cert.getContent()); + } + } + + /** + * Helper method to write the certificates to their own file + * @param filePath the file which needs to be written + * @param content the content which needs to be written to this file + */ + static void writeCertificateToFile(String filePath, String content) { + try { + FileWriter fileWriter = new FileWriter(filePath, StandardCharsets.UTF_8); + fileWriter.write(content); + fileWriter.close(); + } catch (IOException e) { + System.err.println("Error writing certificate file: " + filePath); + System.exit(-1); + } + } +} diff --git a/src/main/java/org/opensearch/security/tools/democonfig/Certificates.java b/src/main/java/org/opensearch/security/tools/democonfig/Certificates.java new file mode 100644 index 0000000000..baff8d7078 --- /dev/null +++ b/src/main/java/org/opensearch/security/tools/democonfig/Certificates.java @@ -0,0 +1,212 @@ +/* + * 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.tools.democonfig; + +import java.util.List; +import java.util.function.Supplier; + +/** + * Enum for demo certificates + */ +public enum Certificates { + ADMIN_CERT( + "kirk.pem", + () -> getCertContent( + List.of( + "-----BEGIN CERTIFICATE-----", + "MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL", + "BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt", + "cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl", + "IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v", + "dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT", + "AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs", + "aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC", + "ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs", + "paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+", + "O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx", + "vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6", + "cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0", + "bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw", + "DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW", + "BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV", + "0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS", + "JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf", + "BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs", + "ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG", + "9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8", + "Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl", + "1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy", + "KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9", + "E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/", + "e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ==", + "-----END CERTIFICATE-----" + ) + ) + ), + ADMIN_CERT_KEY( + "kirk-key.pem", + () -> getCertContent( + List.of( + "-----BEGIN PRIVATE KEY-----", + "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp", + "gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky", + "AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo", + "7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB", + "GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+", + "b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu", + "y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4", + "ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0", + "TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j", + "xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ", + "OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo", + "1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs", + "9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs", + "/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3", + "qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG", + "/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv", + "M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0", + "0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ", + "K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5", + "9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF", + "RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp", + "nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5", + "3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h", + "mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw", + "F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs", + "/AHmo368d4PSNRMMzLHw8Q==", + "-----END PRIVATE KEY-----" + ) + ) + ), + NODE_CERT( + "esnode.pem", + () -> getCertContent( + List.of( + "-----BEGIN CERTIFICATE-----", + "MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL", + "BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt", + "cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl", + "IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v", + "dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT", + "AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl", + "MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA", + "A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud", + "yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0", + "HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr", + "XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n", + "dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD", + "ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R", + "BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA", + "AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF", + "BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo", + "wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ", + "KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz", + "pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi", + "7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh", + "hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L", + "camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg", + "PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg=", + "-----END CERTIFICATE-----" + ) + ) + ), + NODE_KEY( + "esnode-key.pem", + () -> getCertContent( + List.of( + "-----BEGIN PRIVATE KEY-----", + "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv", + "bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0", + "o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50", + "1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1", + "MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b", + "6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa", + "vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo", + "FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ", + "5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O", + "zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ", + "xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow", + "dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn", + "7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U", + "hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej", + "VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B", + "Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c", + "uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy", + "hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv", + "hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/", + "A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh", + "KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX", + "GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f", + "5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud", + "tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71", + "+x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT", + "bg/ch9Rhxbq22yrVgWHh6epp", + "-----END PRIVATE KEY-----" + ) + ) + ), + ROOT_CA( + "root-ca.pem", + () -> getCertContent( + List.of( + "-----BEGIN CERTIFICATE-----", + "MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL", + "BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt", + "cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl", + "IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v", + "dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm", + "iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ", + "RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290", + "IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG", + "SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU", + "j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4", + "U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg", + "vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA", + "WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969", + "VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW", + "MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU", + "F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4", + "uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ", + "k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD", + "VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg", + "Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN", + "AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC", + "YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V", + "6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG", + "1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq", + "qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov", + "rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI=", + "-----END CERTIFICATE-----" + ) + ) + ); + + private final String fileName; + private final Supplier contentSupplier; + + Certificates(String fileName, Supplier contentSupplier) { + this.fileName = fileName; + this.contentSupplier = contentSupplier; + } + + public String getFileName() { + return fileName; + } + + public String getContent() { + return contentSupplier.get(); + } + + private static String getCertContent(List certLines) { + return String.join(System.lineSeparator(), certLines); + } +} diff --git a/src/main/java/org/opensearch/security/tools/democonfig/ExecutionEnvironment.java b/src/main/java/org/opensearch/security/tools/democonfig/ExecutionEnvironment.java new file mode 100644 index 0000000000..e9a8273c5f --- /dev/null +++ b/src/main/java/org/opensearch/security/tools/democonfig/ExecutionEnvironment.java @@ -0,0 +1,20 @@ +/* + * 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.tools.democonfig; + +/** + * The environment in which the demo config installation script is being executed + */ +public enum ExecutionEnvironment { + DEMO, // default value + TEST // to be used only for tests +} diff --git a/src/main/java/org/opensearch/security/tools/democonfig/Installer.java b/src/main/java/org/opensearch/security/tools/democonfig/Installer.java new file mode 100644 index 0000000000..f1ee81f84e --- /dev/null +++ b/src/main/java/org/opensearch/security/tools/democonfig/Installer.java @@ -0,0 +1,445 @@ +/* + * 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.tools.democonfig; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.HashSet; +import java.util.Scanner; +import java.util.Set; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** + * This class installs demo configuration for security plugin + */ +public class Installer { + + // Singleton Pattern + private static Installer instance; + + private static SecuritySettingsConfigurer securitySettingsConfigurer; + + private static CertificateGenerator certificateGenerator; + + boolean assumeyes = false; + boolean initsecurity = false; + boolean cluster_mode = false; + int skip_updates = -1; + String SCRIPT_DIR; + String BASE_DIR; + String OPENSEARCH_CONF_FILE; + String OPENSEARCH_BIN_DIR; + String OPENSEARCH_PLUGINS_DIR; + String OPENSEARCH_LIB_PATH; + String OPENSEARCH_INSTALL_TYPE; + String OPENSEARCH_CONF_DIR; + String OPENSEARCH_VERSION; + String SECURITY_VERSION; + + ExecutionEnvironment environment = ExecutionEnvironment.DEMO; + + String OS; + + final String FILE_EXTENSION; + + static File RPM_DEB_OPENSEARCH_HOME = new File("/usr/share/opensearch"); + + private final Options options; + + // To print help information for this script + private final HelpFormatter formatter = new HelpFormatter(); + + /** + * We do not want this class to be instantiated more than once, + * as we are following Singleton Factory pattern + */ + private Installer() { + this.OS = System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch"); + FILE_EXTENSION = OS.toLowerCase().contains("win") ? ".bat" : ".sh"; + options = new Options(); + } + + /** + * Returns a singleton instance of this class + * @return an existing instance OR a new instance if there was no existing instance + */ + public static Installer getInstance() { + if (instance == null) { + instance = new Installer(); + securitySettingsConfigurer = new SecuritySettingsConfigurer(instance); + certificateGenerator = new CertificateGenerator(instance); + } + return instance; + } + + /** + * Installs the demo security configuration + * @param options the options passed to the script + */ + public void installDemoConfiguration(String[] options) throws IOException { + readOptions(options); + printScriptHeaders(); + gatherUserInputs(); + initializeVariables(); + printVariables(); + securitySettingsConfigurer.configureSecuritySettings(); + certificateGenerator.createDemoCertificates(); + finishScriptExecution(); + } + + public static void main(String[] options) throws IOException { + Installer installer = Installer.getInstance(); + installer.buildOptions(); + installer.installDemoConfiguration(options); + } + + /** + * Builds options supported by this tool + */ + void buildOptions() { + options.addOption("h", "show-help", false, "Shows help for this tool."); + options.addOption("y", "answer-yes-to-all-prompts", false, "Confirm all installation dialogues automatically."); + options.addOption( + "i", + "initialize-security", + false, + "Initialize Security plugin with default configuration (default is to ask if -y is not given)." + ); + options.addOption( + "c", + "enable-cluster-mode", + false, + "Enable cluster mode by binding to all network interfaces (default is to ask if -y is not given)." + ); + options.addOption( + "s", + "skip-updates-when-already-configured", + false, + "Skip updates if config is already applied to opensearch.yml." + ); + options.addOption( + "t", + "test-execution-environment", + false, + "Set the execution environment to `test` to skip password validation. Should be used only for testing. (default is set to `demo`)" + ); + } + + /** + * Prints headers that indicate the start of script execution + */ + static void printScriptHeaders() { + System.out.println("### OpenSearch Security Demo Installer"); + System.out.println("### ** Warning: Do not use on production or public reachable systems **"); + } + + /** + * Reads the options passed to the script + * @param args an array of strings containing options passed to the script + */ + void readOptions(String[] args) { + // set script execution dir + SCRIPT_DIR = args[0]; + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine line = parser.parse(options, args); + + if (line.hasOption("h")) { + showHelp(); + return; + } + assumeyes = line.hasOption("y"); + initsecurity = line.hasOption("i"); + cluster_mode = line.hasOption("c"); + skip_updates = line.hasOption("s") ? 0 : -1; + environment = line.hasOption("t") ? ExecutionEnvironment.TEST : environment; + + } catch (ParseException exp) { + System.out.println("ERR: Parsing failed. Reason: " + exp.getMessage()); + System.exit(-1); + } + } + + /** + * Prints the help menu when -h option is passed + */ + void showHelp() { + formatter.printHelp("install_demo_configuration" + FILE_EXTENSION, options, true); + System.exit(0); + } + + /** + * Prompt the user and collect user inputs + * Input collection will be skipped if -y option was passed + */ + void gatherUserInputs() { + if (!assumeyes) { + try (Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8)) { + + if (!confirmAction(scanner, "Install demo certificates?")) { + System.exit(0); + } + + if (!initsecurity) { + initsecurity = confirmAction(scanner, "Initialize Security Modules?"); + } + + if (!cluster_mode) { + System.out.println("Cluster mode requires additional setup of:"); + System.out.println(" - Virtual memory (vm.max_map_count)" + System.lineSeparator()); + cluster_mode = confirmAction(scanner, "Enable cluster mode?"); + } + } + } + } + + /** + * Helper method to scan user inputs. + * @param scanner object to be used for scanning user input + * @param message prompt question + * @return true or false based on user input + */ + boolean confirmAction(Scanner scanner, String message) { + System.out.print(message + " [y/N] "); + String response = scanner.nextLine(); + return response.equalsIgnoreCase("yes") || response.equalsIgnoreCase("y"); + } + + /** + * Initialize all class level variables required + */ + void initializeVariables() { + setBaseDir(); + setOpenSearchVariables(); + setSecurityVariables(); + } + + /** + * Sets the base directory to be used by the script + */ + void setBaseDir() { + File baseDirFile = new File(SCRIPT_DIR).getParentFile().getParentFile().getParentFile(); + BASE_DIR = baseDirFile != null ? baseDirFile.getAbsolutePath() : null; + + if (BASE_DIR == null || !new File(BASE_DIR).isDirectory()) { + System.out.println("DEBUG: basedir does not exist"); + System.exit(-1); + } + + BASE_DIR += File.separator; + } + + /** + * Sets the variables for items at OpenSearch level + */ + void setOpenSearchVariables() { + OPENSEARCH_CONF_FILE = BASE_DIR + "config" + File.separator + "opensearch.yml"; + OPENSEARCH_BIN_DIR = BASE_DIR + "bin" + File.separator; + OPENSEARCH_PLUGINS_DIR = BASE_DIR + "plugins" + File.separator; + OPENSEARCH_LIB_PATH = BASE_DIR + "lib" + File.separator; + OPENSEARCH_INSTALL_TYPE = determineInstallType(); + + Set errorMessages = validatePaths(); + + if (!errorMessages.isEmpty()) { + errorMessages.forEach(System.out::println); + System.exit(-1); + } + + OPENSEARCH_CONF_DIR = new File(OPENSEARCH_CONF_FILE).getParent(); + OPENSEARCH_CONF_DIR = new File(OPENSEARCH_CONF_DIR).getAbsolutePath() + File.separator; + } + + /** + * Helper method + * Returns a set of error messages for the paths that didn't contain files/directories + * @return a set containing error messages if any, empty otherwise + */ + private Set validatePaths() { + Set errorMessages = new HashSet<>(); + if (!(new File(OPENSEARCH_CONF_FILE).exists())) { + errorMessages.add("Unable to determine OpenSearch config file. Quit."); + } + + if (!(new File(OPENSEARCH_BIN_DIR).exists())) { + errorMessages.add("Unable to determine OpenSearch bin directory. Quit."); + } + + if (!(new File(OPENSEARCH_PLUGINS_DIR).exists())) { + errorMessages.add("Unable to determine OpenSearch plugins directory. Quit."); + } + + if (!(new File(OPENSEARCH_LIB_PATH).exists())) { + errorMessages.add("Unable to determine OpenSearch lib directory. Quit."); + } + return errorMessages; + } + + /** + * Returns the installation type based on the underlying operating system + * @return will be one of `.zip`, `.tar.gz` or `rpm/deb` + */ + String determineInstallType() { + // windows (.bat execution) + if (OS.toLowerCase().contains("win")) { + return ".zip"; + } + + // other OS (.sh execution) + if (RPM_DEB_OPENSEARCH_HOME.exists() && RPM_DEB_OPENSEARCH_HOME.equals(new File(BASE_DIR))) { + OPENSEARCH_CONF_FILE = RPM_DEB_OPENSEARCH_HOME.getAbsolutePath() + "/config/opensearch.yml"; + if (!new File(OPENSEARCH_CONF_FILE).exists()) { + OPENSEARCH_CONF_FILE = "/etc/opensearch/opensearch.yml"; + } + return "rpm/deb"; + } + return ".tar.gz"; + } + + /** + * Sets the path variables for items at OpenSearch security plugin level + */ + void setSecurityVariables() { + if (!(new File(OPENSEARCH_PLUGINS_DIR + "opensearch-security").exists())) { + System.out.println("OpenSearch Security plugin not installed. Quit."); + System.exit(-1); + } + + // Extract OpenSearch version and Security version + File[] opensearchLibFiles = new File(OPENSEARCH_LIB_PATH).listFiles( + pathname -> pathname.getName().matches("opensearch-core-(.*).jar") + ); + + if (opensearchLibFiles != null && opensearchLibFiles.length > 0) { + OPENSEARCH_VERSION = opensearchLibFiles[0].getName().replaceAll("opensearch-core-(.*).jar", "$1"); + } + + File[] securityFiles = new File(OPENSEARCH_PLUGINS_DIR + "opensearch-security").listFiles( + pathname -> pathname.getName().startsWith("opensearch-security-") && pathname.getName().endsWith(".jar") + ); + + if (securityFiles != null && securityFiles.length > 0) { + SECURITY_VERSION = securityFiles[0].getName().replaceAll("opensearch-security-(.*).jar", "$1"); + } + } + + /** + * Prints the initialized variables + */ + void printVariables() { + System.out.println("OpenSearch install type: " + OPENSEARCH_INSTALL_TYPE + " on " + OS); + System.out.println("OpenSearch config dir: " + OPENSEARCH_CONF_DIR); + System.out.println("OpenSearch config file: " + OPENSEARCH_CONF_FILE); + System.out.println("OpenSearch bin dir: " + OPENSEARCH_BIN_DIR); + System.out.println("OpenSearch plugins dir: " + OPENSEARCH_PLUGINS_DIR); + System.out.println("OpenSearch lib dir: " + OPENSEARCH_LIB_PATH); + System.out.println("Detected OpenSearch Version: " + OPENSEARCH_VERSION); + System.out.println("Detected OpenSearch Security Version: " + SECURITY_VERSION); + } + + /** + * Prints end of script execution message and creates security admin demo file. + */ + void finishScriptExecution() { + System.out.println("### Success"); + System.out.println("### Execute this script now on all your nodes and then start all nodes"); + + try { + String securityAdminScriptPath = OPENSEARCH_PLUGINS_DIR + + "opensearch-security" + + File.separator + + "tools" + + File.separator + + "securityadmin" + + FILE_EXTENSION; + String securityAdminDemoScriptPath = OPENSEARCH_CONF_DIR + "securityadmin_demo" + FILE_EXTENSION; + + securitySettingsConfigurer.createSecurityAdminDemoScript(securityAdminScriptPath, securityAdminDemoScriptPath); + + // Make securityadmin_demo script executable + // not needed for windows + if (!OS.toLowerCase().contains("win")) { + Path file = Paths.get(securityAdminDemoScriptPath); + Set perms = new HashSet<>(); + // Add the execute permission for owner, group, and others + perms.add(PosixFilePermission.OWNER_READ); + perms.add(PosixFilePermission.OWNER_EXECUTE); + perms.add(PosixFilePermission.GROUP_EXECUTE); + perms.add(PosixFilePermission.OTHERS_EXECUTE); + Files.setPosixFilePermissions(file, perms); + } + + // Read the last line of the security-admin script + String lastLine = ""; + try (BufferedReader reader = new BufferedReader(new FileReader(securityAdminDemoScriptPath, StandardCharsets.UTF_8))) { + String currentLine; + while ((currentLine = reader.readLine()) != null) { + lastLine = currentLine; + } + } + + if (!initsecurity) { + System.out.println("### After the whole cluster is up execute: "); + System.out.println(lastLine); + System.out.println("### or run ." + File.separator + "securityadmin_demo" + FILE_EXTENSION); + System.out.println("### After that you can also use the Security Plugin ConfigurationGUI"); + } else { + System.out.println("### OpenSearch Security will be automatically initialized."); + System.out.println("### If you like to change the runtime configuration "); + System.out.println( + "### change the files in .." + + File.separator + + ".." + + File.separator + + ".." + + File.separator + + "config" + + File.separator + + "opensearch-security and execute: " + ); + System.out.println(lastLine); + System.out.println("### or run ." + File.separator + "securityadmin_demo" + FILE_EXTENSION); + System.out.println("### To use the Security Plugin ConfigurationGUI"); + } + + System.out.println( + "### To access your secured cluster open https://: and log in with admin/." + ); + System.out.println("### (Ignore the SSL certificate warning because we installed self-signed demo certificates)"); + + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + /** + * FOR TESTS ONLY + * resets the installer state to allow testing with fresh instance for the next test. + */ + static void resetInstance() { + instance = null; + } +} diff --git a/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java new file mode 100644 index 0000000000..72f0247e53 --- /dev/null +++ b/src/main/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurer.java @@ -0,0 +1,399 @@ +/* + * 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.tools.democonfig; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import org.bouncycastle.crypto.generators.OpenBSDBCrypt; + +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; +import org.opensearch.security.dlic.rest.validation.PasswordValidator; +import org.opensearch.security.dlic.rest.validation.RequestContentValidator; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.tools.Hasher; + +import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import static org.opensearch.security.DefaultObjectMapper.YAML_MAPPER; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH; +import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX; + +/** + * This class updates the security related configuration, as needed. + */ +public class SecuritySettingsConfigurer { + + static final List REST_ENABLED_ROLES = List.of("all_access", "security_rest_api_access"); + static final List SYSTEM_INDICES = List.of( + ".plugins-ml-agent", + ".plugins-ml-config", + ".plugins-ml-connector", + ".plugins-ml-controller", + ".plugins-ml-model-group", + ".plugins-ml-model", + ".plugins-ml-task", + ".plugins-ml-conversation-meta", + ".plugins-ml-conversation-interactions", + ".plugins-ml-memory-meta", + ".plugins-ml-memory-message", + ".plugins-ml-stop-words", + ".opendistro-alerting-config", + ".opendistro-alerting-alert*", + ".opendistro-anomaly-results*", + ".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*", + ".plugins-flow-framework-config", + ".plugins-flow-framework-templates", + ".plugins-flow-framework-state" + ); + static final Integer DEFAULT_PASSWORD_MIN_LENGTH = 8; + static String ADMIN_PASSWORD = ""; + static String ADMIN_USERNAME = "admin"; + + private final Installer installer; + static final String DEFAULT_ADMIN_PASSWORD = "admin"; + + public SecuritySettingsConfigurer(Installer installer) { + this.installer = installer; + } + + /** + * Configures security related changes to the opensearch configuration + * 1. Checks if plugins is already configuration. If yes, exit + * 2. Sets the custom admin password (Generates one if none is provided) + * 3. Write the security config to opensearch.yml + */ + public void configureSecuritySettings() throws IOException { + checkIfSecurityPluginIsAlreadyConfigured(); + updateAdminPassword(); + writeSecurityConfigToOpenSearchYML(); + } + + /** + * Checks if security plugin is already configured. If so, the script execution will exit. + */ + void checkIfSecurityPluginIsAlreadyConfigured() { + // Check if the configuration file contains the 'plugins.security' string + if (installer.OPENSEARCH_CONF_FILE != null && new File(installer.OPENSEARCH_CONF_FILE).exists()) { + try (BufferedReader br = new BufferedReader(new FileReader(installer.OPENSEARCH_CONF_FILE, StandardCharsets.UTF_8))) { + String line; + while ((line = br.readLine()) != null) { + if (line.toLowerCase().contains("plugins.security")) { + System.out.println(installer.OPENSEARCH_CONF_FILE + " seems to be already configured for Security. Quit."); + System.exit(installer.skip_updates); + } + } + } catch (IOException e) { + System.err.println("Error reading configuration file."); + System.exit(-1); + } + } else { + System.err.println("OpenSearch configuration file does not exist. Quit."); + System.exit(-1); + } + } + + /** + * Replaces the admin password in internal_users.yml with the custom or generated password + */ + void updateAdminPassword() throws IOException { + String INTERNAL_USERS_FILE_PATH = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; + boolean shouldValidatePassword = installer.environment.equals(ExecutionEnvironment.DEMO); + + // check if the password `admin` is present, if not skip updating admin password + if (!isAdminPasswordSetToAdmin(INTERNAL_USERS_FILE_PATH)) { + System.out.println("Admin password seems to be custom configured. Skipping update to admin password."); + return; + } + + // if hashed value for default password "admin" is found, update it with the custom password. + try { + final PasswordValidator passwordValidator = PasswordValidator.of( + Settings.builder() + .put(SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, "(?=.*[A-Z])(?=.*[^a-zA-Z\\\\d])(?=.*[0-9])(?=.*[a-z]).{8,}") + .put(SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, DEFAULT_PASSWORD_MIN_LENGTH) + .build() + ); + + // Read custom password from environment variable + String initialAdminPassword = System.getenv().get(ConfigConstants.OPENSEARCH_INITIAL_ADMIN_PASSWORD); + if (!Strings.isNullOrEmpty(initialAdminPassword)) { + ADMIN_PASSWORD = initialAdminPassword; + } + + // If script execution environment is set to demo, validate custom password, else if set to test, skip validation + if (shouldValidatePassword && !ADMIN_PASSWORD.isEmpty()) { + RequestContentValidator.ValidationError response = passwordValidator.validate(ADMIN_USERNAME, ADMIN_PASSWORD); + if (!RequestContentValidator.ValidationError.NONE.equals(response)) { + System.out.println( + String.format( + "Password %s failed validation: \"%s\". Please re-try with a minimum %d character password and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character that is strong. Password strength can be tested here: https://lowe.github.io/tryzxcvbn", + ADMIN_PASSWORD, + response.message(), + DEFAULT_PASSWORD_MIN_LENGTH + ) + ); + System.exit(-1); + } + } + + // if ADMIN_PASSWORD is still an empty string, it implies no custom password was provided. We exit the setup. + if (Strings.isNullOrEmpty(ADMIN_PASSWORD)) { + System.out.println( + String.format( + "No custom admin password found. Please provide a password via the environment variable %s.", + ConfigConstants.OPENSEARCH_INITIAL_ADMIN_PASSWORD + ) + ); + System.exit(-1); + } + + // Update the custom password in internal_users.yml file + writePasswordToInternalUsersFile(ADMIN_PASSWORD, INTERNAL_USERS_FILE_PATH); + + System.out.println("Admin password set successfully."); + + } catch (IOException e) { + System.out.println("Exception updating the admin password : " + e.getMessage()); + System.exit(-1); + } + } + + /** + * Check if the password for admin user was already updated. (Possibly via a custom internal_users.yml) + * @param internalUsersFile Path to internal_users.yml file + * @return true if password was already updated, false otherwise + * @throws IOException if there was an error while reading the file + */ + private boolean isAdminPasswordSetToAdmin(String internalUsersFile) throws IOException { + JsonNode internalUsers = YAML_MAPPER.readTree(new FileInputStream(internalUsersFile)); + return internalUsers.has("admin") + && OpenBSDBCrypt.checkPassword(internalUsers.get("admin").get("hash").asText(), DEFAULT_ADMIN_PASSWORD.toCharArray()); + } + + /** + * Generate password hash and update it in the internal_users.yml file + * @param adminPassword the password to be hashed and updated + * @param internalUsersFile the file path string to internal_users.yml file + * @throws IOException while reading, writing to files + */ + void writePasswordToInternalUsersFile(String adminPassword, String internalUsersFile) throws IOException { + String hashedAdminPassword = Hasher.hash(adminPassword.toCharArray()); + + if (hashedAdminPassword.isEmpty()) { + System.out.println("Failure while hashing the admin password, see console for details."); + System.exit(-1); + } + + try { + var map = YAML_MAPPER.readValue(new File(internalUsersFile), new TypeReference>>() { + }); + var admin = map.get("admin"); + if (admin != null) { + // Replace the password since the default password was found via the check: isAdminPasswordSetToAdmin(..) + admin.put("hash", hashedAdminPassword); + } + + // Write the updated map back to the internal_users.yml file + YAML_MAPPER.writeValue(new File(internalUsersFile), map); + } catch (IOException e) { + throw new IOException("Unable to update the internal users file with the hashed password."); + } + } + + /** + * Update opensearch.yml with security configuration information + */ + void writeSecurityConfigToOpenSearchYML() { + String configHeader = System.lineSeparator() + + System.lineSeparator() + + "######## Start OpenSearch Security Demo Configuration ########" + + System.lineSeparator() + + "# WARNING: revise all the lines below before you go into production" + + System.lineSeparator(); + String configFooter = "######## End OpenSearch Security Demo Configuration ########" + System.lineSeparator(); + + Map securityConfigAsMap = buildSecurityConfigMap(); + + try (FileWriter writer = new FileWriter(installer.OPENSEARCH_CONF_FILE, StandardCharsets.UTF_8, true)) { + writer.write(configHeader); + Yaml yaml = new Yaml(); + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + String yamlString = yaml.dump(securityConfigAsMap); + writer.write(yamlString); + writer.write(configFooter); + } catch (IOException e) { + System.err.println("Exception writing security configuration to opensearch.yml : " + e.getMessage()); + System.exit(-1); + } + } + + /** + * Helper method to build security configuration to append to opensearch.yml + * @return the configuration map to be written to opensearch.yml + */ + Map buildSecurityConfigMap() { + Map configMap = new LinkedHashMap<>(); + + configMap.put("plugins.security.ssl.transport.pemcert_filepath", Certificates.NODE_CERT.getFileName()); + configMap.put("plugins.security.ssl.transport.pemkey_filepath", Certificates.NODE_KEY.getFileName()); + configMap.put("plugins.security.ssl.transport.pemtrustedcas_filepath", Certificates.ROOT_CA.getFileName()); + configMap.put("plugins.security.ssl.transport.enforce_hostname_verification", false); + configMap.put("plugins.security.ssl.http.enabled", true); + configMap.put("plugins.security.ssl.http.pemcert_filepath", Certificates.NODE_CERT.getFileName()); + configMap.put("plugins.security.ssl.http.pemkey_filepath", Certificates.NODE_KEY.getFileName()); + configMap.put("plugins.security.ssl.http.pemtrustedcas_filepath", Certificates.ROOT_CA.getFileName()); + configMap.put("plugins.security.allow_unsafe_democertificates", true); + + if (installer.initsecurity) { + configMap.put("plugins.security.allow_default_init_securityindex", true); + } + + configMap.put("plugins.security.authcz.admin_dn", List.of("CN=kirk,OU=client,O=client,L=test,C=de")); + + configMap.put("plugins.security.audit.type", "internal_opensearch"); + configMap.put("plugins.security.enable_snapshot_restore_privilege", true); + configMap.put("plugins.security.check_snapshot_restore_write_privileges", true); + configMap.put("plugins.security.restapi.roles_enabled", REST_ENABLED_ROLES); + + configMap.put("plugins.security.system_indices.enabled", true); + configMap.put("plugins.security.system_indices.indices", SYSTEM_INDICES); + + if (!isNetworkHostAlreadyPresent(installer.OPENSEARCH_CONF_FILE)) { + if (installer.cluster_mode) { + configMap.put("network.host", "0.0.0.0"); + configMap.put("node.name", "smoketestnode"); + configMap.put("cluster.initial_cluster_manager_nodes", "smoketestnode"); + } + } + + if (!isNodeMaxLocalStorageNodesAlreadyPresent(installer.OPENSEARCH_CONF_FILE)) { + configMap.put("node.max_local_storage_nodes", 3); + } + + return configMap; + } + + /** + * Helper method to check if network.host config is present + * @param filePath path to opensearch.yml + * @return true is present, false otherwise + */ + static boolean isNetworkHostAlreadyPresent(String filePath) { + try { + String searchString = "network.host"; + return isKeyPresentInYMLFile(filePath, searchString); + } catch (IOException e) { + return false; + } + } + + /** + * Helper method to check if node.max_local_storage_nodes config is present + * @param filePath path to opensearch.yml + * @return true if present, false otherwise + */ + static boolean isNodeMaxLocalStorageNodesAlreadyPresent(String filePath) { + try { + String searchString = "node.max_local_storage_nodes"; + return isKeyPresentInYMLFile(filePath, searchString); + } catch (IOException e) { + return false; + } + } + + /** + * Checks if the given key is present in the yml file + * @param filePath path to yml file in which given key should be searched + * @param key the key to be searched for + * @return true if the key is present, false otherwise + * @throws IOException if there was exception reading the file + */ + static boolean isKeyPresentInYMLFile(String filePath, String key) throws IOException { + JsonNode node; + try { + node = YAML_MAPPER.readTree(new File(filePath)); + } catch (IOException e) { + throw new RuntimeException(e); + } + + return node.has(key); + } + + /** + * Helper method to create security_admin_demo.(sh|bat) + * @param securityAdminScriptPath path to original script + * @param securityAdminDemoScriptPath path to security admin demo script + * @throws IOException if there was error reading/writing the file + */ + void createSecurityAdminDemoScript(String securityAdminScriptPath, String securityAdminDemoScriptPath) throws IOException { + String[] securityAdminCommands = getSecurityAdminCommands(securityAdminScriptPath); + + // Write securityadmin_demo script + FileWriter writer = new FileWriter(securityAdminDemoScriptPath, StandardCharsets.UTF_8); + for (String command : securityAdminCommands) { + writer.write(command + System.lineSeparator()); + } + writer.close(); + } + + /** + * Return the command to be added to securityadmin_demo script + * @param securityAdminScriptPath the path to securityadmin.(sh|bat) + * @return the command string + */ + String[] getSecurityAdminCommands(String securityAdminScriptPath) { + String securityAdminExecutionPath = securityAdminScriptPath + + "\" -cd \"" + + installer.OPENSEARCH_CONF_DIR + + "opensearch-security\" -icl -key \"" + + installer.OPENSEARCH_CONF_DIR + + Certificates.ADMIN_CERT_KEY.getFileName() + + "\" -cert \"" + + installer.OPENSEARCH_CONF_DIR + + Certificates.ADMIN_CERT.getFileName() + + "\" -cacert \"" + + installer.OPENSEARCH_CONF_DIR + + Certificates.ROOT_CA.getFileName() + + "\" -nhnv"; + + if (installer.OS.toLowerCase().contains("win")) { + return new String[] { "@echo off", "call \"" + securityAdminExecutionPath }; + } + + return new String[] { "#!/bin/bash", "sudo" + " \"" + securityAdminExecutionPath }; + } +} diff --git a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java index fe1094c411..f55d9ac338 100644 --- a/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java +++ b/src/main/java/org/opensearch/security/transport/SecurityInterceptor.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.function.Supplier; import java.util.stream.Collectors; import com.google.common.collect.Maps; @@ -61,6 +62,7 @@ import org.opensearch.security.support.Base64Helper; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.HeaderHelper; +import org.opensearch.security.support.SerializationFormat; import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.Transport.Connection; @@ -71,8 +73,6 @@ import org.opensearch.transport.TransportRequestOptions; import org.opensearch.transport.TransportResponseHandler; -import static org.opensearch.security.OpenSearchSecurityPlugin.isActionTraceEnabled; - public class SecurityInterceptor { protected final Logger log = LogManager.getLogger(getClass()); @@ -86,6 +86,7 @@ public class SecurityInterceptor { private final SslExceptionHandler sslExceptionHandler; private final ClusterInfoHolder clusterInfoHolder; private final SSLConfig SSLConfig; + private final Supplier actionTraceEnabled; public SecurityInterceptor( final Settings settings, @@ -97,7 +98,8 @@ public SecurityInterceptor( final ClusterService cs, final SslExceptionHandler sslExceptionHandler, final ClusterInfoHolder clusterInfoHolder, - final SSLConfig SSLConfig + final SSLConfig SSLConfig, + final Supplier actionTraceSupplier ) { this.backendRegistry = backendRegistry; this.auditLog = auditLog; @@ -109,6 +111,7 @@ public SecurityInterceptor( this.sslExceptionHandler = sslExceptionHandler; this.clusterInfoHolder = clusterInfoHolder; this.SSLConfig = SSLConfig; + this.actionTraceEnabled = actionTraceSupplier; } public SecurityRequestHandler getHandler(String action, TransportRequestHandler actualHandler) { @@ -148,7 +151,8 @@ 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 var serializationFormat = SerializationFormat.determineFormat(connection.getVersion()); final boolean isSameNodeRequest = localNode != null && localNode.equals(connection.getNode()); try (ThreadContext.StoredContext stashedContext = getThreadContext().stashContext()) { @@ -226,17 +230,20 @@ && 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); + try { + if (serializationFormat == SerializationFormat.JDK) { + 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); + } catch (IllegalArgumentException iae) { + log.debug("Failed to add headers information onto on thread context", iae); } - getThreadContext().putHeader(headerMap); - ensureCorrectHeaders( remoteAddress0, user0, @@ -244,10 +251,10 @@ && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROL injectedUserString, injectedRolesString, isSameNodeRequest, - useJDKSerialization + serializationFormat ); - if (isActionTraceEnabled()) { + if (actionTraceEnabled.get()) { getThreadContext().putHeader( "_opendistro_security_trace" + System.currentTimeMillis() + "#" + UUID.randomUUID().toString(), Thread.currentThread().getName() @@ -273,7 +280,7 @@ private void ensureCorrectHeaders( final String injectedUserString, final String injectedRolesString, final boolean isSameNodeRequest, - final boolean useJDKSerialization + final SerializationFormat format ) { // keep original address @@ -311,6 +318,7 @@ && getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADE getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, injectedUserString); } } else { + final var useJDKSerialization = format == SerializationFormat.JDK; if (transportAddress != null) { getThreadContext().putHeader( ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER, @@ -407,5 +415,4 @@ public String executor() { return innerHandler.executor(); } } - } diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index aa9c09a469..6abba3d734 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -229,7 +229,7 @@ public final boolean equals(final Object obj) { if (obj == null) { return false; } - if (getClass() != obj.getClass()) { + if (!(obj instanceof User)) { return false; } final User other = (User) obj; diff --git a/src/main/java/org/opensearch/security/util/ratetracking/HeapBasedRateTracker.java b/src/main/java/org/opensearch/security/util/ratetracking/HeapBasedRateTracker.java index 40b1f622d0..46aa577254 100644 --- a/src/main/java/org/opensearch/security/util/ratetracking/HeapBasedRateTracker.java +++ b/src/main/java/org/opensearch/security/util/ratetracking/HeapBasedRateTracker.java @@ -18,8 +18,10 @@ package org.opensearch.security.util.ratetracking; import java.util.Arrays; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.function.LongSupplier; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; @@ -33,16 +35,22 @@ public class HeapBasedRateTracker implements RateTracker cache; + private final LongSupplier timeProvider; private final long timeWindowMs; private final int maxTimeOffsets; public HeapBasedRateTracker(long timeWindowMs, int allowedTries, int maxEntries) { + this(timeWindowMs, allowedTries, maxEntries, null); + } + + public HeapBasedRateTracker(long timeWindowMs, int allowedTries, int maxEntries, LongSupplier timeProvider) { if (allowedTries < 2) { throw new IllegalArgumentException("allowedTries must be >= 2"); } this.timeWindowMs = timeWindowMs; this.maxTimeOffsets = allowedTries > 2 ? allowedTries - 2 : 0; + this.timeProvider = Optional.ofNullable(timeProvider).orElse(System::currentTimeMillis); this.cache = CacheBuilder.newBuilder() .expireAfterAccess(this.timeWindowMs, TimeUnit.MILLISECONDS) .maximumSize(maxEntries) @@ -89,7 +97,7 @@ private class ClientRecord { private short timeOffsetEnd = -1; synchronized boolean track() { - long timestamp = System.currentTimeMillis(); + long timestamp = timeProvider.getAsLong(); if (this.startTime == -1 || timestamp - getMostRecent() >= timeWindowMs) { this.startTime = timestamp; diff --git a/src/main/resources/static_config/static_roles.yml b/src/main/resources/static_config/static_roles.yml index d688848a6e..c7820ab627 100644 --- a/src/main/resources/static_config/static_roles.yml +++ b/src/main/resources/static_config/static_roles.yml @@ -132,6 +132,7 @@ logstash: index_permissions: - index_patterns: - "logstash-*" + - "ecs-logstash-*" allowed_actions: - "create_index" - "crud" diff --git a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SelfRefreshingKeySetTest.java b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SelfRefreshingKeySetTest.java index bab23c5fc4..ef53ba5ec0 100644 --- a/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SelfRefreshingKeySetTest.java +++ b/src/test/java/com/amazon/dlic/auth/http/jwt/keybyoidc/SelfRefreshingKeySetTest.java @@ -11,66 +11,94 @@ package com.amazon.dlic.auth.http.jwt.keybyoidc; +import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; -import com.nimbusds.jose.jwk.JWK; import com.nimbusds.jose.jwk.JWKSet; import com.nimbusds.jose.jwk.OctetSequenceKey; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.fail; + public class SelfRefreshingKeySetTest { - @Test - public void basicTest() throws AuthenticatorUnavailableException, BadCredentialsException { - SelfRefreshingKeySet selfRefreshingKeySet = new SelfRefreshingKeySet(new MockKeySetProvider()); - - OctetSequenceKey key1 = (OctetSequenceKey) selfRefreshingKeySet.getKey("kid/a"); - Assert.assertEquals(TestJwk.OCT_1_K, key1.getKeyValue().decodeToString()); - Assert.assertEquals(1, selfRefreshingKeySet.getRefreshCount()); - - OctetSequenceKey key2 = (OctetSequenceKey) selfRefreshingKeySet.getKey("kid/b"); - Assert.assertEquals(TestJwk.OCT_2_K, key2.getKeyValue().decodeToString()); - Assert.assertEquals(1, selfRefreshingKeySet.getRefreshCount()); - - try { - selfRefreshingKeySet.getKey("kid/X"); - Assert.fail("Expected a BadCredentialsException"); - } catch (BadCredentialsException e) { - Assert.assertEquals(2, selfRefreshingKeySet.getRefreshCount()); - } + private SelfRefreshingKeySet selfRefreshingKeySet; + private String keyForKidA; + private String keyForKidB; + private int numThreads = 10; + @Before + public void setUp() throws AuthenticatorUnavailableException, BadCredentialsException { + selfRefreshingKeySet = new SelfRefreshingKeySet(new MockKeySetProvider()); + keyForKidA = TestJwk.OCT_1_K; + keyForKidB = TestJwk.OCT_2_K; } - @Test(timeout = 10000) - public void twoThreadedTest() throws Exception { - BlockingMockKeySetProvider provider = new BlockingMockKeySetProvider(); + @Test + public void getKey_withKidShouldReturnValidKey() throws AuthenticatorUnavailableException, BadCredentialsException { - final SelfRefreshingKeySet selfRefreshingKeySet = new SelfRefreshingKeySet(provider); + OctetSequenceKey key = (OctetSequenceKey) selfRefreshingKeySet.getKey("kid/a"); + assertThat(keyForKidA, is(equalTo(key.getKeyValue().decodeToString()))); + } - ExecutorService executorService = Executors.newCachedThreadPool(); + @Test + public void getKey__withNullOrInvalidKidShouldThrowAnException() throws AuthenticatorUnavailableException, BadCredentialsException { - Future f1 = executorService.submit(() -> selfRefreshingKeySet.getKey("kid/a")); + Assert.assertThrows(AuthenticatorUnavailableException.class, () -> selfRefreshingKeySet.getKey(null)); + Assert.assertThrows(BadCredentialsException.class, () -> selfRefreshingKeySet.getKey("kid/X")); + } - provider.waitForCalled(); + @Test + public void getKeyAfterRefresh_withKidShouldReturnKey() throws AuthenticatorUnavailableException, BadCredentialsException { - Future f2 = executorService.submit(() -> selfRefreshingKeySet.getKey("kid/b")); + OctetSequenceKey key = (OctetSequenceKey) selfRefreshingKeySet.getKeyAfterRefresh("kid/b"); + assertThat(keyForKidB, is(equalTo(key.getKeyValue().decodeToString()))); + } - while (selfRefreshingKeySet.getQueuedGetCount() == 0) { - Thread.sleep(10); + @Test + public void getKeyAfterRefresh_withMultipleCallsShouldIncreaseQueueCount() throws InterruptedException, ExecutionException { + ExecutorService executor = Executors.newFixedThreadPool(numThreads); + String[] keys = new String[] { "kid/a", "kid/b" }; + for (int i = 0; i < numThreads; i++) { + // Using executor to make multiple asynchronous calls to method getKeyAfterRefresh, so queuedGetCount gets increased. + // Without executor block, getKeyAfterRefresh method would be called once on each iteration in the main thread and wait for the + // task to complete before continuing the loop, so queuedGetCount would not have pending tasks. + executor.execute(() -> { + try { + int indexKey = (int) (Math.random() * 2); + String keyToCompare = indexKey == 0 ? keyForKidA : keyForKidB; + OctetSequenceKey key = (OctetSequenceKey) selfRefreshingKeySet.getKeyAfterRefresh(keys[indexKey]); + + assertThat(key, is(notNullValue())); + assertThat(keyToCompare, is(equalTo(key.getKeyValue().decodeToString()))); + } catch (AuthenticatorUnavailableException | BadCredentialsException e) { + fail("No exception was expected but found: " + e.getMessage()); + } + }); } - provider.unblock(); + executor.shutdown(); + executor.awaitTermination(1, TimeUnit.SECONDS); - Assert.assertEquals(TestJwk.OCT_1_K, ((OctetSequenceKey) f1.get()).getKeyValue().decodeToString()); - Assert.assertEquals(TestJwk.OCT_2_K, ((OctetSequenceKey) f2.get()).getKeyValue().decodeToString()); + assertThat((int) selfRefreshingKeySet.getRefreshCount(), is(greaterThan(0))); + assertThat((int) selfRefreshingKeySet.getQueuedGetCount(), is(greaterThan(0))); + } - Assert.assertEquals(1, selfRefreshingKeySet.getRefreshCount()); - Assert.assertEquals(1, selfRefreshingKeySet.getQueuedGetCount()); + @Test + public void getKeyAfterRefresh_withNullOrInvalidKidShouldThrowBadCredentialsException() { + Assert.assertThrows(BadCredentialsException.class, () -> selfRefreshingKeySet.getKeyAfterRefresh(null)); + Assert.assertThrows(BadCredentialsException.class, () -> selfRefreshingKeySet.getKeyAfterRefresh("kid/X")); } static class MockKeySetProvider implements KeySetProvider { @@ -79,42 +107,5 @@ static class MockKeySetProvider implements KeySetProvider { public JWKSet get() throws AuthenticatorUnavailableException { return TestJwk.OCT_1_2_3; } - - } - - static class BlockingMockKeySetProvider extends MockKeySetProvider { - private boolean blocked = true; - private boolean called = false; - - @Override - public synchronized JWKSet get() throws AuthenticatorUnavailableException { - - called = true; - notifyAll(); - - waitForUnblock(); - - return super.get(); - } - - public synchronized void unblock() { - blocked = false; - notifyAll(); - } - - public synchronized void waitForCalled() throws InterruptedException { - while (!called) { - wait(); - } - } - - private synchronized void waitForUnblock() { - while (blocked) { - try { - wait(); - } catch (InterruptedException e) {} - - } - } } } 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 c76a1b546d..bba2ee8b5c 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 @@ -887,7 +887,7 @@ private AuthenticateHeaders getAutenticateHeaders(HTTPSamlAuthenticator samlAuth RestRequest restRequest = new FakeRestRequest(ImmutableMap.of(), new HashMap()); SecurityResponse response = sendToAuthenticator(samlAuthenticator, restRequest).orElseThrow(); - String wwwAuthenticateHeader = response.getHeaders().get("WWW-Authenticate"); + String wwwAuthenticateHeader = response.getHeaders().get("WWW-Authenticate").get(0); Assert.assertNotNull(wwwAuthenticateHeader); diff --git a/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendIntegTest.java b/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendIntegTest.java index 6fda346a93..24389a1086 100644 --- a/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendIntegTest.java +++ b/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendIntegTest.java @@ -11,8 +11,8 @@ package com.amazon.dlic.auth.ldap; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; diff --git a/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendTest.java b/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendTest.java index 4fe7ad0514..8e5e2541b8 100755 --- a/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendTest.java +++ b/src/test/java/com/amazon/dlic/auth/ldap/LdapBackendTest.java @@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Paths; import java.util.Arrays; +import java.util.List; import org.hamcrest.MatcherAssert; import org.junit.AfterClass; @@ -39,6 +40,7 @@ import org.ldaptive.ReturnAttributes; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; public class LdapBackendTest { @@ -579,6 +581,90 @@ public void testLdapAuthorizationNested() throws Exception { MatcherAssert.assertThat(user.getRoles(), hasItem("nested1")); } + @Test + public void testLdapNestedRoleFiltering() { + + final Settings settings = Settings.builder() + .putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapPort) + .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") + .put(ConfigConstants.LDAP_AUTHC_USERBASE, "ou=people,o=TEST") + .put(ConfigConstants.LDAP_AUTHZ_ROLEBASE, "ou=groups,o=TEST") + .put(ConfigConstants.LDAP_AUTHZ_ROLENAME, "cn") + .put(ConfigConstants.LDAP_AUTHZ_RESOLVE_NESTED_ROLES, true) + .put(ConfigConstants.LDAP_AUTHZ_ROLESEARCH, "(uniqueMember={0})") + .putList(ConfigConstants.LDAP_AUTHZ_EXCLUDE_ROLES, List.of("nested1", "nested2")) + .build(); + + final User user = new User("spock"); + + new LDAPAuthorizationBackend(settings, null).fillRoles(user, null); + + Assert.assertNotNull(user); + Assert.assertEquals("spock", user.getName()); + Assert.assertEquals(2, user.getRoles().size()); + // filtered out + MatcherAssert.assertThat(user.getRoles(), not(hasItem("nested1"))); + MatcherAssert.assertThat(user.getRoles(), not(hasItem("nested2"))); + MatcherAssert.assertThat(user.getRoles(), hasItem("role2")); + MatcherAssert.assertThat(user.getRoles(), hasItem("ceo")); + } + + @Test + public void testLdapNestedRoleFilteringWithExcludedRolesWildcard() { + + final Settings settings = Settings.builder() + .putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapPort) + .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") + .put(ConfigConstants.LDAP_AUTHC_USERBASE, "ou=people,o=TEST") + .put(ConfigConstants.LDAP_AUTHZ_ROLEBASE, "ou=groups,o=TEST") + .put(ConfigConstants.LDAP_AUTHZ_ROLENAME, "cn") + .put(ConfigConstants.LDAP_AUTHZ_RESOLVE_NESTED_ROLES, true) + .put(ConfigConstants.LDAP_AUTHZ_ROLESEARCH, "(uniqueMember={0})") + .putList(ConfigConstants.LDAP_AUTHZ_EXCLUDE_ROLES, List.of("nested*")) + .build(); + + final User user = new User("spock"); + + new LDAPAuthorizationBackend(settings, null).fillRoles(user, null); + + Assert.assertNotNull(user); + Assert.assertEquals("spock", user.getName()); + Assert.assertEquals(2, user.getRoles().size()); + // filtered out + MatcherAssert.assertThat(user.getRoles(), not(hasItem("nested1"))); + MatcherAssert.assertThat(user.getRoles(), not(hasItem("nested2"))); + MatcherAssert.assertThat(user.getRoles(), hasItem("role2")); + MatcherAssert.assertThat(user.getRoles(), hasItem("ceo")); + } + + @Test + public void testLdapdRoleFiltering() { + + final Settings settings = Settings.builder() + .putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapPort) + .put(ConfigConstants.LDAP_AUTHC_USERSEARCH, "(uid={0})") + .put(ConfigConstants.LDAP_AUTHC_USERBASE, "ou=people,o=TEST") + .put(ConfigConstants.LDAP_AUTHZ_ROLEBASE, "ou=groups,o=TEST") + .put(ConfigConstants.LDAP_AUTHZ_ROLENAME, "cn") + .put(ConfigConstants.LDAP_AUTHZ_RESOLVE_NESTED_ROLES, true) + .put(ConfigConstants.LDAP_AUTHZ_ROLESEARCH, "(uniqueMember={0})") + .putList(ConfigConstants.LDAP_AUTHZ_EXCLUDE_ROLES, List.of("ceo", "role1", "role2")) + .build(); + + final User user = new User("spock"); + + new LDAPAuthorizationBackend(settings, null).fillRoles(user, null); + + Assert.assertNotNull(user); + Assert.assertEquals("spock", user.getName()); + Assert.assertEquals(2, user.getRoles().size()); + MatcherAssert.assertThat(user.getRoles(), hasItem("nested1")); + MatcherAssert.assertThat(user.getRoles(), hasItem("nested2")); + // filtered out + MatcherAssert.assertThat(user.getRoles(), not(hasItem("role2"))); + MatcherAssert.assertThat(user.getRoles(), not(hasItem("ceo"))); + } + @Test public void testLdapAuthorizationNestedFilter() throws Exception { diff --git a/src/test/java/com/amazon/dlic/auth/ldap/srv/LdapServer.java b/src/test/java/com/amazon/dlic/auth/ldap/srv/LdapServer.java index 36bb37494d..64cc1f8ab0 100644 --- a/src/test/java/com/amazon/dlic/auth/ldap/srv/LdapServer.java +++ b/src/test/java/com/amazon/dlic/auth/ldap/srv/LdapServer.java @@ -23,9 +23,12 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.locks.ReentrantLock; +import java.util.logging.Handler; +import java.util.logging.LogRecord; import com.google.common.io.CharStreams; import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -153,9 +156,8 @@ private synchronized int configureAndStartServer(String... ldifFiles) throws Exc config.setEnforceAttributeSyntaxCompliance(false); config.setEnforceSingleStructuralObjectClass(false); - // config.setLDAPDebugLogHandler(DEBUG_HANDLER); - // config.setAccessLogHandler(DEBUG_HANDLER); - // config.addAdditionalBindCredentials(configuration.getBindDn(), configuration.getPassword()); + config.setLDAPDebugLogHandler(new ServerLogger()); + config.setAccessLogHandler(new ServerLogger()); server = new InMemoryDirectoryServer(config); @@ -214,25 +216,33 @@ private int loadLdifFiles(String... ldifFiles) throws Exception { return ldifLoadCount; } - /* private static class DebugHandler extends Handler { - private final static Logger LOG = LogManager.getLogger(DebugHandler.class); + private static class ServerLogger extends Handler { + final Logger logger = LogManager.getLogger(ServerLogger.class); @Override - public void publish(LogRecord logRecord) { - //LOG.debug(ToStringBuilder.reflectionToString(logRecord, ToStringStyle.MULTI_LINE_STYLE)); + public void publish(final LogRecord logRecord) { + logger.log(toLog4jLevel(logRecord.getLevel()), logRecord.getMessage(), logRecord.getThrown()); } @Override - public void flush() { - - } + public void flush() {} @Override - public void close() throws SecurityException { - + public void close() throws SecurityException {} + + private Level toLog4jLevel(java.util.logging.Level javaLevel) { + switch (javaLevel.getName()) { + case "SEVERE": + return Level.ERROR; + case "WARNING": + return Level.WARN; + case "INFO": + return Level.INFO; + case "CONFIG": + return Level.DEBUG; + default: + return Level.TRACE; + } } } - - private static final DebugHandler DEBUG_HANDLER = new DebugHandler(); - */ } diff --git a/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendIntegTest2.java b/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendIntegTest2.java index 6f0958790a..e4f71ff264 100644 --- a/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendIntegTest2.java +++ b/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendIntegTest2.java @@ -11,8 +11,8 @@ package com.amazon.dlic.auth.ldap2; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; diff --git a/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendTestNewStyleConfig2.java b/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendTestNewStyleConfig2.java index 634584c167..6f23e4ab44 100644 --- a/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendTestNewStyleConfig2.java +++ b/src/test/java/com/amazon/dlic/auth/ldap2/LdapBackendTestNewStyleConfig2.java @@ -15,6 +15,7 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.TreeSet; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -564,16 +565,18 @@ public void testLdapAuthorizationNested() throws Exception { .put(ConfigConstants.LDAP_AUTHZ_ROLENAME, "cn") .put(ConfigConstants.LDAP_AUTHZ_RESOLVE_NESTED_ROLES, true) .put("roles.g1.search", "(uniqueMember={0})") + .putList(ConfigConstants.LDAP_AUTHZ_EXCLUDE_ROLES, List.of("nested2")) .build(); final User user = new User("spock"); - new LDAPAuthorizationBackend(settings, null).fillRoles(user, null); + new LDAPAuthorizationBackend2(settings, null).fillRoles(user, null); Assert.assertNotNull(user); Assert.assertEquals("spock", user.getName()); - Assert.assertEquals(4, user.getRoles().size()); - Assert.assertEquals("nested1", new ArrayList<>(new TreeSet<>(user.getRoles())).get(1)); + Assert.assertEquals(3, user.getRoles().size()); + Assert.assertTrue(user.getRoles().contains("nested1")); + Assert.assertFalse(user.getRoles().contains("nested2")); } @Test @@ -759,7 +762,7 @@ public void testLdapAuthorizationNestedAttrFilter() throws Exception { } @Test - public void testLdapAuthorizationNestedAttrFilterAll() throws Exception { + public void testLdapAuthorizationNestedAttrFilterAll() { final Settings settings = createBaseSettings().putList(ConfigConstants.LDAP_HOSTS, "localhost:" + ldapPort) .put("users.u1.search", "(uid={0})") @@ -780,7 +783,6 @@ public void testLdapAuthorizationNestedAttrFilterAll() throws Exception { Assert.assertNotNull(user); Assert.assertEquals("spock", user.getName()); Assert.assertEquals(4, user.getRoles().size()); - } @Test diff --git a/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java b/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java index e8ac049385..5cf9485892 100644 --- a/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java +++ b/src/test/java/org/opensearch/security/AdvancedSecurityMigrationTests.java @@ -15,7 +15,7 @@ import java.util.Arrays; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.After; import org.junit.Assert; import org.junit.Before; diff --git a/src/test/java/org/opensearch/security/AggregationTests.java b/src/test/java/org/opensearch/security/AggregationTests.java index a61d5d169d..c6591125d5 100644 --- a/src/test/java/org/opensearch/security/AggregationTests.java +++ b/src/test/java/org/opensearch/security/AggregationTests.java @@ -26,7 +26,7 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java b/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java index 773244c7ea..2f4e665001 100644 --- a/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java +++ b/src/test/java/org/opensearch/security/DataStreamIntegrationTests.java @@ -11,7 +11,7 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/EncryptionInTransitMigrationTests.java b/src/test/java/org/opensearch/security/EncryptionInTransitMigrationTests.java index 462cd591e6..a028f2d43d 100644 --- a/src/test/java/org/opensearch/security/EncryptionInTransitMigrationTests.java +++ b/src/test/java/org/opensearch/security/EncryptionInTransitMigrationTests.java @@ -10,7 +10,7 @@ */ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/HealthTests.java b/src/test/java/org/opensearch/security/HealthTests.java index c36440f1a2..385757ea53 100644 --- a/src/test/java/org/opensearch/security/HealthTests.java +++ b/src/test/java/org/opensearch/security/HealthTests.java @@ -26,7 +26,7 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/HttpIntegrationTests.java b/src/test/java/org/opensearch/security/HttpIntegrationTests.java index 60abaf8efe..3a437ea80a 100644 --- a/src/test/java/org/opensearch/security/HttpIntegrationTests.java +++ b/src/test/java/org/opensearch/security/HttpIntegrationTests.java @@ -31,9 +31,9 @@ import java.nio.file.Files; import com.fasterxml.jackson.databind.JsonNode; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.NoHttpResponseException; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/IndexIntegrationTests.java b/src/test/java/org/opensearch/security/IndexIntegrationTests.java index 3d024f28f7..a5c137d61e 100644 --- a/src/test/java/org/opensearch/security/IndexIntegrationTests.java +++ b/src/test/java/org/opensearch/security/IndexIntegrationTests.java @@ -31,7 +31,7 @@ import java.util.Date; import java.util.TimeZone; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java b/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java index 03d26e2062..e08367d2b2 100644 --- a/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java +++ b/src/test/java/org/opensearch/security/IndexTemplateClusterPermissionsCheckTest.java @@ -11,7 +11,7 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/InitializationIntegrationTests.java b/src/test/java/org/opensearch/security/InitializationIntegrationTests.java index 78b03a5fab..7545822620 100644 --- a/src/test/java/org/opensearch/security/InitializationIntegrationTests.java +++ b/src/test/java/org/opensearch/security/InitializationIntegrationTests.java @@ -32,9 +32,9 @@ import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http2.HttpVersionPolicy; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -302,12 +302,6 @@ public void testInvalidDefaultConfig() throws Exception { HttpStatus.SC_SERVICE_UNAVAILABLE, rh.executeGetRequest("", encodeBasicHeader("admin", "admin")).getStatusCode() ); - - ClusterHelper.updateDefaultDirectory(defaultInitDirectory); - restart(Settings.EMPTY, null, settings, false); - rh = nonSslRestHelper(); - Thread.sleep(10000); - Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("admin", "admin")).getStatusCode()); } finally { ClusterHelper.resetSystemProperties(); } diff --git a/src/test/java/org/opensearch/security/IntegrationTests.java b/src/test/java/org/opensearch/security/IntegrationTests.java index 31a46be331..0777594834 100644 --- a/src/test/java/org/opensearch/security/IntegrationTests.java +++ b/src/test/java/org/opensearch/security/IntegrationTests.java @@ -29,8 +29,8 @@ import java.util.TreeSet; import com.fasterxml.jackson.databind.JsonNode; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Assume; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/PitIntegrationTests.java b/src/test/java/org/opensearch/security/PitIntegrationTests.java index 035cc2ce3e..c1c25fcf9c 100644 --- a/src/test/java/org/opensearch/security/PitIntegrationTests.java +++ b/src/test/java/org/opensearch/security/PitIntegrationTests.java @@ -13,7 +13,7 @@ import java.util.ArrayList; import java.util.List; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/ResolveAPITests.java b/src/test/java/org/opensearch/security/ResolveAPITests.java index 088702acd9..765d933432 100644 --- a/src/test/java/org/opensearch/security/ResolveAPITests.java +++ b/src/test/java/org/opensearch/security/ResolveAPITests.java @@ -15,7 +15,7 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.junit.Assert; diff --git a/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java b/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java index b8da89e2dc..99cf3b82fe 100644 --- a/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java +++ b/src/test/java/org/opensearch/security/SecurityAdminIEndpointsTests.java @@ -11,7 +11,7 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/SecurityAdminInvalidConfigsTests.java b/src/test/java/org/opensearch/security/SecurityAdminInvalidConfigsTests.java index 6cb89dc18f..1586878b9f 100644 --- a/src/test/java/org/opensearch/security/SecurityAdminInvalidConfigsTests.java +++ b/src/test/java/org/opensearch/security/SecurityAdminInvalidConfigsTests.java @@ -30,7 +30,7 @@ import java.util.ArrayList; import java.util.List; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/SecurityAdminTests.java b/src/test/java/org/opensearch/security/SecurityAdminTests.java index 760f2a33d7..d2b7dab37d 100644 --- a/src/test/java/org/opensearch/security/SecurityAdminTests.java +++ b/src/test/java/org/opensearch/security/SecurityAdminTests.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.Objects; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/SecurityRolesTests.java b/src/test/java/org/opensearch/security/SecurityRolesTests.java index 24a6bafbb8..0b4dd0b95b 100644 --- a/src/test/java/org/opensearch/security/SecurityRolesTests.java +++ b/src/test/java/org/opensearch/security/SecurityRolesTests.java @@ -26,8 +26,8 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/SlowIntegrationTests.java b/src/test/java/org/opensearch/security/SlowIntegrationTests.java index 28ec9f8d88..eb147ec422 100644 --- a/src/test/java/org/opensearch/security/SlowIntegrationTests.java +++ b/src/test/java/org/opensearch/security/SlowIntegrationTests.java @@ -29,13 +29,14 @@ import java.io.IOException; import com.google.common.collect.Lists; -import org.apache.hc.core5.http.HttpStatus; +import org.awaitility.Awaitility; import org.junit.Assert; import org.junit.Test; import org.opensearch.action.admin.cluster.health.ClusterHealthRequest; import org.opensearch.action.admin.cluster.node.info.NodesInfoRequest; import org.opensearch.action.admin.cluster.settings.ClusterUpdateSettingsRequest; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; import org.opensearch.cluster.health.ClusterHealthStatus; import org.opensearch.common.settings.Settings; import org.opensearch.common.unit.TimeValue; @@ -50,6 +51,9 @@ import org.opensearch.security.test.helper.rest.RestHelper; import org.opensearch.transport.Netty4ModulePlugin; +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertThrows; + public class SlowIntegrationTests extends SingleClusterTest { @Test @@ -223,27 +227,34 @@ public void testDelayInSecurityIndexInitialization() throws Exception { .put(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true) .put("cluster.routing.allocation.exclude._ip", "127.0.0.1") .build(); - try { + assertThrows(IOException.class, () -> { setup(Settings.EMPTY, null, settings, false); - Assert.fail("Expected IOException here due to red cluster state"); - } catch (IOException e) { - // Index request has a default timeout of 1 minute, adding buffer between nodes initialization and cluster health check - Thread.sleep(1000 * 80); - // Ideally, we would want to remove this cluster setting, but default settings cannot be removed. So overriding with a reserved - // IP address clusterHelper.nodeClient() .admin() - .cluster() - .updateSettings( - new ClusterUpdateSettingsRequest().transientSettings( - Settings.builder().put("cluster.routing.allocation.exclude._ip", "192.0.2.0").build() - ) - ); - this.clusterInfo = clusterHelper.waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), 3); - } + .indices() + .create(new CreateIndexRequest("test-index").timeout(TimeValue.timeValueSeconds(10))) + .actionGet(); + clusterHelper.waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(5), ClusterConfiguration.DEFAULT.getNodes()); + }); + // Ideally, we would want to remove this cluster setting, but default settings cannot be removed. So overriding with a reserved + // IP address + clusterHelper.nodeClient() + .admin() + .cluster() + .updateSettings( + new ClusterUpdateSettingsRequest().transientSettings( + Settings.builder().put("cluster.routing.allocation.exclude._ip", "192.0.2.0").build() + ) + ); + this.clusterInfo = clusterHelper.waitForCluster(ClusterHealthStatus.GREEN, TimeValue.timeValueSeconds(10), 3); RestHelper rh = nonSslRestHelper(); - Thread.sleep(10000); - Assert.assertEquals(HttpStatus.SC_OK, rh.executeGetRequest("", encodeBasicHeader("admin", "admin")).getStatusCode()); + Awaitility.await() + .alias("Wait until Security is initialized") + .until( + () -> rh.executeGetRequest("/_plugins/_security/health", encodeBasicHeader("admin", "admin")) + .getTextFromJsonBody("/status"), + equalTo("UP") + ); } } diff --git a/src/test/java/org/opensearch/security/SnapshotRestoreTests.java b/src/test/java/org/opensearch/security/SnapshotRestoreTests.java index 1c884a8e5d..1e9c26d898 100644 --- a/src/test/java/org/opensearch/security/SnapshotRestoreTests.java +++ b/src/test/java/org/opensearch/security/SnapshotRestoreTests.java @@ -29,7 +29,7 @@ import java.util.Arrays; import java.util.List; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/SystemIntegratorsTests.java b/src/test/java/org/opensearch/security/SystemIntegratorsTests.java index 27e44b1ce5..b927ceaba2 100644 --- a/src/test/java/org/opensearch/security/SystemIntegratorsTests.java +++ b/src/test/java/org/opensearch/security/SystemIntegratorsTests.java @@ -27,8 +27,8 @@ package org.opensearch.security; import com.google.common.collect.Lists; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/TaskTests.java b/src/test/java/org/opensearch/security/TaskTests.java index 39bb21f164..e58fa5c6a9 100644 --- a/src/test/java/org/opensearch/security/TaskTests.java +++ b/src/test/java/org/opensearch/security/TaskTests.java @@ -17,8 +17,8 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/TracingTests.java b/src/test/java/org/opensearch/security/TracingTests.java index 55dccdee35..7ae663a41f 100644 --- a/src/test/java/org/opensearch/security/TracingTests.java +++ b/src/test/java/org/opensearch/security/TracingTests.java @@ -26,7 +26,7 @@ package org.opensearch.security; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java b/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java index b13e5fbb20..decd886e15 100644 --- a/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java +++ b/src/test/java/org/opensearch/security/TransportUserInjectorIntegTest.java @@ -14,6 +14,7 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; +import java.util.List; import java.util.function.Supplier; import com.google.common.collect.Lists; @@ -26,7 +27,9 @@ import org.opensearch.client.Client; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.Setting; import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; import org.opensearch.core.common.io.stream.NamedWriteableRegistry; import org.opensearch.core.xcontent.NamedXContentRegistry; import org.opensearch.env.Environment; @@ -47,9 +50,10 @@ public class TransportUserInjectorIntegTest extends SingleClusterTest { + public static final String TEST_INJECTED_USER = "test_injected_user"; + public static class UserInjectorPlugin extends Plugin implements ActionPlugin { Settings settings; - public static String injectedUser = null; public UserInjectorPlugin(final Settings settings, final Path configPath) { this.settings = settings; @@ -69,17 +73,24 @@ public Collection createComponents( IndexNameExpressionResolver indexNameExpressionResolver, Supplier repositoriesServiceSupplier ) { - if (injectedUser != null) threadPool.getThreadContext() - .putTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, injectedUser); + if (!Strings.isNullOrEmpty(settings.get(TEST_INJECTED_USER))) threadPool.getThreadContext() + .putTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_USER, settings.get(TEST_INJECTED_USER)); return new ArrayList<>(); } + + @Override + public List> getSettings() { + List> settings = new ArrayList>(); + settings.add(Setting.simpleString(TEST_INJECTED_USER, Setting.Property.NodeScope, Setting.Property.Filtered)); + return settings; + } } @Test public void testSecurityUserInjection() throws Exception { final Settings clusterNodeSettings = Settings.builder().put(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, true).build(); setup(clusterNodeSettings, new DynamicSecurityConfig().setSecurityRolesMapping("roles_transport_inject_user.yml"), Settings.EMPTY); - final Settings tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) + final Settings.Builder tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) .put(minimumSecuritySettings(Settings.EMPTY).get(0)) .put("cluster.name", clusterInfo.clustername) .put("path.data", "./target/data/" + clusterInfo.clustername + "/cert/data") @@ -89,14 +100,13 @@ public void testSecurityUserInjection() throws Exception { .put("discovery.initial_state_timeout", "8s") .put("plugins.security.allow_default_init_securityindex", "true") .put(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, true) - .putList("discovery.zen.ping.unicast.hosts", clusterInfo.nodeHost + ":" + clusterInfo.nodePort) - .build(); + .putList("discovery.zen.ping.unicast.hosts", clusterInfo.nodeHost + ":" + clusterInfo.nodePort); // 1. without user injection try ( Node node = new PluginAwareNode( false, - tcSettings, + tcSettings.build(), Lists.newArrayList(Netty4ModulePlugin.class, OpenSearchSecurityPlugin.class, UserInjectorPlugin.class) ).start() ) { @@ -106,12 +116,11 @@ public void testSecurityUserInjection() throws Exception { } // 2. with invalid backend roles - UserInjectorPlugin.injectedUser = "ttt|kkk"; Exception exception = null; try ( Node node = new PluginAwareNode( false, - tcSettings, + tcSettings.put(TEST_INJECTED_USER, "ttt|kkk").build(), Lists.newArrayList(Netty4ModulePlugin.class, OpenSearchSecurityPlugin.class, UserInjectorPlugin.class) ).start() ) { @@ -126,11 +135,10 @@ public void testSecurityUserInjection() throws Exception { } // 3. with valid backend roles for injected user - UserInjectorPlugin.injectedUser = "injectedadmin|injecttest"; try ( Node node = new PluginAwareNode( false, - tcSettings, + tcSettings.put(TEST_INJECTED_USER, "injectedadmin|injecttest").build(), Lists.newArrayList(Netty4ModulePlugin.class, OpenSearchSecurityPlugin.class, UserInjectorPlugin.class) ).start() ) { @@ -146,7 +154,7 @@ public void testSecurityUserInjectionWithConfigDisabled() throws Exception { .put(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false) .build(); setup(clusterNodeSettings, new DynamicSecurityConfig().setSecurityRolesMapping("roles_transport_inject_user.yml"), Settings.EMPTY); - final Settings tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) + final Settings.Builder tcSettings = AbstractSecurityUnitTest.nodeRolesSettings(Settings.builder(), false, false) .put(minimumSecuritySettings(Settings.EMPTY).get(0)) .put("cluster.name", clusterInfo.clustername) .put("path.data", "./target/data/" + clusterInfo.clustername + "/cert/data") @@ -156,14 +164,13 @@ public void testSecurityUserInjectionWithConfigDisabled() throws Exception { .put("discovery.initial_state_timeout", "8s") .put("plugins.security.allow_default_init_securityindex", "true") .put(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false) - .putList("discovery.zen.ping.unicast.hosts", clusterInfo.nodeHost + ":" + clusterInfo.nodePort) - .build(); + .putList("discovery.zen.ping.unicast.hosts", clusterInfo.nodeHost + ":" + clusterInfo.nodePort); // 1. without user injection try ( Node node = new PluginAwareNode( false, - tcSettings, + tcSettings.build(), Lists.newArrayList(Netty4ModulePlugin.class, OpenSearchSecurityPlugin.class, UserInjectorPlugin.class) ).start() ) { @@ -173,11 +180,10 @@ public void testSecurityUserInjectionWithConfigDisabled() throws Exception { } // with invalid backend roles - UserInjectorPlugin.injectedUser = "ttt|kkk"; try ( Node node = new PluginAwareNode( false, - tcSettings, + tcSettings.put(TEST_INJECTED_USER, "ttt|kkk").build(), Lists.newArrayList(Netty4ModulePlugin.class, OpenSearchSecurityPlugin.class, UserInjectorPlugin.class) ).start() ) { diff --git a/src/test/java/org/opensearch/security/UtilTests.java b/src/test/java/org/opensearch/security/UtilTests.java index 3b6ed2edc9..402d5dc92f 100644 --- a/src/test/java/org/opensearch/security/UtilTests.java +++ b/src/test/java/org/opensearch/security/UtilTests.java @@ -70,6 +70,8 @@ public void testWildcardMatcherClasses() { assertTrue(wc("/\\S+/").test("abc")); assertTrue(wc("abc").test("abc")); assertFalse(wc("ABC").test("abc")); + assertFalse(wc(null).test("abc")); + assertTrue(WildcardMatcher.from(null, "abc").test("abc")); } @Test diff --git a/src/test/java/org/opensearch/security/auditlog/AbstractAuditlogiUnitTest.java b/src/test/java/org/opensearch/security/auditlog/AbstractAuditlogiUnitTest.java index 3d814231cf..b671378ad4 100644 --- a/src/test/java/org/opensearch/security/auditlog/AbstractAuditlogiUnitTest.java +++ b/src/test/java/org/opensearch/security/auditlog/AbstractAuditlogiUnitTest.java @@ -84,34 +84,28 @@ protected void setupStarfleetIndex() { rh.keystore = keystore; } - protected boolean validateMsgs(final Collection msgs) { - boolean valid = true; + protected void validateMsgs(final Collection msgs) throws Exception { for (AuditMessage msg : msgs) { - valid = validateMsg(msg) && valid; + validateMsg(msg); } - return valid; - } - protected boolean validateMsg(final AuditMessage msg) { - return validateJson(msg.toJson()) && validateJson(msg.toPrettyString()); } - protected boolean validateJson(final String json) { + protected void validateMsg(final AuditMessage msg) throws Exception { + validateJson(msg.toJson()); + validateJson(msg.toPrettyString()); + } + protected void validateJson(final String json) throws Exception { // this function can throw either IllegalArgumentException, + // JsonMappingException if (json == null || json.isEmpty()) { - return false; + throw new IllegalArgumentException("json is either null or empty"); } - try { - JsonNode node = DefaultObjectMapper.objectMapper.readTree(json); - - if (node.get("audit_request_body") != null) { - DefaultObjectMapper.objectMapper.readTree(node.get("audit_request_body").asText()); - } + JsonNode node = DefaultObjectMapper.objectMapper.readTree(json); - return true; - } catch (Exception e) { - return false; + if (node.get("audit_request_body") != null) { + DefaultObjectMapper.objectMapper.readTree(node.get("audit_request_body").asText()); } } diff --git a/src/test/java/org/opensearch/security/auditlog/AuditTestUtils.java b/src/test/java/org/opensearch/security/auditlog/AuditTestUtils.java index 98f5fab88e..ad3f6afbce 100644 --- a/src/test/java/org/opensearch/security/auditlog/AuditTestUtils.java +++ b/src/test/java/org/opensearch/security/auditlog/AuditTestUtils.java @@ -15,7 +15,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.opensearch.client.Client; import org.opensearch.cluster.metadata.IndexNameExpressionResolver; diff --git a/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceAuditlogTest.java b/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceAuditlogTest.java index 773180bd1b..5ba95dc756 100644 --- a/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceAuditlogTest.java +++ b/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceAuditlogTest.java @@ -18,7 +18,7 @@ import com.google.common.collect.ImmutableMap; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -97,7 +97,7 @@ public void testSourceFilter() throws Exception { assertThat(message.getRequestBody(), not(containsString("Salary"))); assertThat(message.getRequestBody(), containsString("Gender")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test @@ -245,7 +245,7 @@ public void testSourceFilterMsearch() throws Exception { assertThat(genderMsg.getRequestBody(), containsString("Gender")); assertThat(genderMsg.getRequestBody(), not(containsString("Salary"))); - Assert.assertTrue(validateMsgs(messages)); + validateMsgs(messages); } @Test @@ -302,7 +302,7 @@ public void testInternalConfig() throws Exception { ); }); - Assert.assertTrue(validateMsgs(messages)); + validateMsgs(messages); } @Test @@ -346,7 +346,7 @@ public void testExternalConfig() throws Exception { assertThat(messages.get(1).getNodeId(), not(equalTo(messages.get(2).getNodeId()))); assertThat(messages.get(2).getNodeId(), not(equalTo(messages.get(3).getNodeId()))); - Assert.assertTrue(validateMsgs(messages)); + validateMsgs(messages); } @Test @@ -399,7 +399,7 @@ public void testUpdate() throws Exception { assertThat(ex2.getMissingCount(), equalTo(1)); Assert.assertTrue(TestAuditlogImpl.messages.isEmpty()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test diff --git a/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceConfigTest.java b/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceConfigTest.java index 467475212b..302d26bb00 100644 --- a/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceConfigTest.java +++ b/src/test/java/org/opensearch/security/auditlog/compliance/ComplianceConfigTest.java @@ -12,8 +12,12 @@ package org.opensearch.security.auditlog.compliance; import java.util.Collections; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; import com.google.common.collect.ImmutableSet; +import org.apache.logging.log4j.Logger; import org.junit.Test; import org.opensearch.common.settings.Settings; @@ -21,10 +25,22 @@ import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.support.WildcardMatcher; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.mockito.Mockito; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; public class ComplianceConfigTest { @@ -136,4 +152,84 @@ public void testEmpty() { assertSame(WildcardMatcher.NONE, complianceConfig.getIgnoredComplianceUsersForReadMatcher()); assertSame(WildcardMatcher.NONE, complianceConfig.getIgnoredComplianceUsersForWriteMatcher()); } + + @Test + public void testLogState() { + // arrange + final var logger = Mockito.mock(Logger.class); + final ComplianceConfig complianceConfig = ComplianceConfig.from(Settings.EMPTY); + // act + complianceConfig.log(logger); + // assert: don't validate content, but ensure message's logged is generally consistant + verify(logger, times(6)).info(anyString(), anyString()); + verify(logger, times(1)).info(anyString(), isNull(String.class)); + verify(logger, times(1)).info(anyString(), any(Map.class)); + verify(logger, times(3)).info(anyString(), any(WildcardMatcher.class)); + verifyNoMoreInteractions(logger); + } + + @Test + public void testReadWriteHistoryEnabledForIndex_rollingIndex() { + // arrange + final var date = new AtomicReference(); + final Consumer setYear = (year) -> { date.set(new DateTime(year, 1, 1, 1, 2, DateTimeZone.UTC)); }; + final ComplianceConfig complianceConfig = ComplianceConfig.from( + Settings.builder() + .put( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, + "'audit-log-index'-YYYY-MM-dd" + ) + .put(ConfigConstants.SECURITY_AUDIT_TYPE_DEFAULT, "internal_opensearch") + .putList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, "*") + .putList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, "*") + .build(), + date::get + ); + + // act: Don't log for null indices + assertThat(complianceConfig.readHistoryEnabledForIndex(null), equalTo(false)); + assertThat(complianceConfig.writeHistoryEnabledForIndex(null), equalTo(false)); + // act: Don't log for the security indices + assertThat(complianceConfig.readHistoryEnabledForIndex(complianceConfig.getSecurityIndex()), equalTo(false)); + assertThat(complianceConfig.writeHistoryEnabledForIndex(complianceConfig.getSecurityIndex()), equalTo(false)); + + // act: Don't log for the current audit log + setYear.accept(1337); + assertThat(complianceConfig.readHistoryEnabledForIndex("audit-log-index-1337-01-01"), equalTo(false)); + assertThat(complianceConfig.writeHistoryEnabledForIndex("audit-log-index-1337-01-01"), equalTo(false)); + + // act: Log for current audit log when it does not match the date + setYear.accept(2048); + // See https://github.com/opensearch-project/security/issues/3950 + // assertThat(complianceConfig.readHistoryEnabledForIndex("audit-log-index-1337-01-01"), equalTo(true)); + assertThat(complianceConfig.writeHistoryEnabledForIndex("audit-log-index-1337-01-01"), equalTo(true)); + + // act: Log for any matching index + assertThat(complianceConfig.readHistoryEnabledForIndex("my-data"), equalTo(true)); + assertThat(complianceConfig.writeHistoryEnabledForIndex("my-data"), equalTo(true)); + } + + @Test + public void testReadWriteHistoryEnabledForIndex_staticIndex() { + // arrange + final ComplianceConfig complianceConfig = ComplianceConfig.from( + Settings.builder() + .put( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, + "audit-log-index" + ) + .put(ConfigConstants.SECURITY_AUDIT_TYPE_DEFAULT, "internal_opensearch") + .putList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, "*") + .putList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, "*") + .build() + ); + + // act: Don't log for the static audit log + assertThat(complianceConfig.readHistoryEnabledForIndex(complianceConfig.getAuditLogIndex()), equalTo(false)); + assertThat(complianceConfig.writeHistoryEnabledForIndex(complianceConfig.getAuditLogIndex()), equalTo(false)); + + // act: Log for any matching index + assertThat(complianceConfig.readHistoryEnabledForIndex("my-data"), equalTo(true)); + assertThat(complianceConfig.writeHistoryEnabledForIndex("my-data"), equalTo(true)); + } } diff --git a/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java b/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java index cf06726ea1..784176e1dd 100644 --- a/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java +++ b/src/test/java/org/opensearch/security/auditlog/compliance/RestApiComplianceAuditlogTest.java @@ -11,18 +11,26 @@ package org.opensearch.security.auditlog.compliance; -import org.apache.hc.core5.http.HttpStatus; +import java.util.List; +import java.util.stream.Collectors; + +import org.apache.http.HttpStatus; import org.junit.Assert; -import org.junit.Ignore; import org.junit.Test; import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.AbstractAuditlogiUnitTest; +import org.opensearch.security.auditlog.impl.AuditCategory; import org.opensearch.security.auditlog.impl.AuditMessage; import org.opensearch.security.auditlog.integration.TestAuditlogImpl; import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.test.helper.cluster.ClusterConfiguration; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; + public class RestApiComplianceAuditlogTest extends AbstractAuditlogiUnitTest { @Test @@ -40,22 +48,19 @@ public void testRestApiRolesEnabled() throws Exception { .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, "authenticated,GRANTED_PRIVILEGES") .build(); - setup(additionalSettings); - TestAuditlogImpl.clear(); - String body = "{ \"password\":\"some new password\",\"backend_roles\":[\"role1\",\"role2\"] }"; - HttpResponse response = rh.executePutRequest( - "_opendistro/_security/api/internalusers/compuser?pretty", - body, - encodeBasicHeader("admin", "admin") - ); - Thread.sleep(1500); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - Assert.assertTrue(TestAuditlogImpl.messages.size() + "", TestAuditlogImpl.messages.size() == 1); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("audit_request_effective_user")); - Assert.assertFalse(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_INTERNAL_CONFIG_READ")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_INTERNAL_CONFIG_WRITE")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("UPDATE")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + setupAndReturnAuditMessages(additionalSettings); + final AuditMessage message = TestAuditlogImpl.doThenWaitForMessage(() -> { + String body = "{ \"password\":\"some new password\",\"backend_roles\":[\"role1\",\"role2\"] }"; + HttpResponse response = rh.executePutRequest( + "_opendistro/_security/api/internalusers/compuser?pretty", + body, + encodeBasicHeader("admin", "admin") + ); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + }); + validateMsgs(List.of(message)); + + assertThat(message.toString(), containsString("UPDATE")); } @Test @@ -72,8 +77,7 @@ public void testRestApiRolesDisabled() throws Exception { .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, "authenticated,GRANTED_PRIVILEGES") .build(); - setup(additionalSettings); - TestAuditlogImpl.clear(); + setupAndReturnAuditMessages(additionalSettings); String body = "{ \"password\":\"some new password\",\"backend_roles\":[\"role1\",\"role2\"] }"; rh.enableHTTPClientSSL = true; @@ -81,19 +85,15 @@ public void testRestApiRolesDisabled() throws Exception { rh.sendAdminCertificate = true; rh.keystore = "kirk-keystore.jks"; - HttpResponse response = rh.executePutRequest("_opendistro/_security/api/internalusers/compuser?pretty", body); - Thread.sleep(1500); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - Assert.assertTrue(TestAuditlogImpl.messages.size() + "", TestAuditlogImpl.messages.size() == 1); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("audit_request_effective_user")); - Assert.assertFalse(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_INTERNAL_CONFIG_READ")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_INTERNAL_CONFIG_WRITE")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("UPDATE")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + final AuditMessage message = TestAuditlogImpl.doThenWaitForMessage(() -> { + HttpResponse response = rh.executePutRequest("_opendistro/_security/api/internalusers/compuser?pretty", body); + Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); + }); + validateMsgs(List.of(message)); + assertThat(message.toString(), containsString("COMPLIANCE_INTERNAL_CONFIG_WRITE")); } @Test - @Ignore public void testRestApiRolesDisabledGet() throws Exception { Settings additionalSettings = Settings.builder() @@ -107,22 +107,19 @@ public void testRestApiRolesDisabledGet() throws Exception { .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, "authenticated,GRANTED_PRIVILEGES") .build(); - setup(additionalSettings); - TestAuditlogImpl.clear(); + setupAndReturnAuditMessages(additionalSettings); rh.enableHTTPClientSSL = true; rh.trustHTTPServerCertificate = true; rh.sendAdminCertificate = true; rh.keystore = "kirk-keystore.jks"; - HttpResponse response = rh.executeGetRequest("_opendistro/_security/api/rolesmapping/opendistro_security_all_access?pretty"); - Thread.sleep(1500); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Assert.assertTrue(TestAuditlogImpl.messages.size() > 2); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("audit_request_effective_user")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_INTERNAL_CONFIG_READ")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_INTERNAL_CONFIG_WRITE")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("UPDATE")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + final AuditMessage message = TestAuditlogImpl.doThenWaitForMessage(() -> { + HttpResponse response = rh.executeGetRequest("_opendistro/_security/api/rolesmapping/opendistro_security_all_access?pretty"); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + }); + validateMsgs(List.of(message)); + assertThat(message.toString(), containsString("audit_request_effective_user")); + assertThat(message.toString(), containsString("COMPLIANCE_INTERNAL_CONFIG_READ")); } @Test @@ -139,15 +136,13 @@ public void testAutoInit() throws Exception { .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, "authenticated,GRANTED_PRIVILEGES") .build(); - setup(additionalSettings); + final List messages = setupAndReturnAuditMessages(additionalSettings); - Thread.sleep(1500); - - Assert.assertTrue(TestAuditlogImpl.messages.size() > 2); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("audit_request_effective_user")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_INTERNAL_CONFIG_WRITE")); - Assert.assertTrue(TestAuditlogImpl.sb.toString().contains("COMPLIANCE_EXTERNAL_CONFIG")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(messages); + String allMessages = messages.stream().map(AuditMessage::toString).collect(Collectors.joining(",")); + assertThat(allMessages, containsString("audit_request_effective_user")); + assertThat(allMessages, containsString("COMPLIANCE_INTERNAL_CONFIG_WRITE")); + assertThat(allMessages, containsString("COMPLIANCE_EXTERNAL_CONFIG")); } @Test @@ -161,20 +156,22 @@ public void testRestApiNewUser() throws Exception { .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS, false) .put(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, false) .put(ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, true) - .put(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS, "admin") .build(); - setup(additionalSettings); - TestAuditlogImpl.clear(); - String body = "{ \"password\":\"some new password\",\"backend_roles\":[\"role1\",\"role2\"] }"; - HttpResponse response = rh.executePutRequest( - "_opendistro/_security/api/internalusers/compuser?pretty", - body, - encodeBasicHeader("admin", "admin") - ); - Thread.sleep(1500); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - Assert.assertTrue(TestAuditlogImpl.messages.size() + "", TestAuditlogImpl.messages.isEmpty()); + setupAndReturnAuditMessages(additionalSettings); + + final AuditMessage message = TestAuditlogImpl.doThenWaitForMessage(() -> { + String body = "{ \"password\":\"some new password\",\"backend_roles\":[\"role1\",\"role2\"] }"; + HttpResponse response = rh.executePutRequest( + "_opendistro/_security/api/internalusers/compuser?pretty", + body, + encodeBasicHeader("admin", "admin") + ); + Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); + }); + validateMsgs(List.of(message)); + assertThat(message.toString(), containsString("audit_request_effective_user")); + assertThat(message.toString(), containsString("COMPLIANCE_INTERNAL_CONFIG_WRITE")); } @Test @@ -192,23 +189,21 @@ public void testRestInternalConfigRead() throws Exception { .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, "authenticated,GRANTED_PRIVILEGES") .build(); - setup(additionalSettings); - TestAuditlogImpl.clear(); + setupAndReturnAuditMessages(additionalSettings); rh.enableHTTPClientSSL = true; rh.trustHTTPServerCertificate = true; rh.sendAdminCertificate = true; rh.keystore = "kirk-keystore.jks"; - HttpResponse response = rh.executeGetRequest("_opendistro/_security/api/internalusers/admin?pretty"); - Thread.sleep(1500); - String auditLogImpl = TestAuditlogImpl.sb.toString(); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Assert.assertTrue(TestAuditlogImpl.messages.size() + "", TestAuditlogImpl.messages.size() == 1); - Assert.assertTrue(auditLogImpl.contains("audit_request_effective_user")); - Assert.assertTrue(auditLogImpl.contains("COMPLIANCE_INTERNAL_CONFIG_READ")); - Assert.assertFalse(auditLogImpl.contains("COMPLIANCE_INTERNAL_CONFIG_WRITE")); - Assert.assertFalse(auditLogImpl.contains("UPDATE")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + + final AuditMessage message = TestAuditlogImpl.doThenWaitForMessage(() -> { + HttpResponse response = rh.executeGetRequest("_opendistro/_security/api/internalusers/admin?pretty"); + String auditLogImpl = TestAuditlogImpl.sb.toString(); + Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); + Assert.assertTrue(auditLogImpl.contains("COMPLIANCE_INTERNAL_CONFIG_READ")); + }); + validateMsgs(List.of(message)); + assertThat(message.toString(), containsString("COMPLIANCE_INTERNAL_CONFIG_READ")); } @Test @@ -221,26 +216,63 @@ public void testBCryptHashRedaction() throws Exception { .put(ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, true) .put(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, true) .build(); - setup(settings); + setupAndReturnAuditMessages(settings); rh.sendAdminCertificate = true; rh.keystore = "kirk-keystore.jks"; // read internal users and verify no BCrypt hash is present in audit logs - TestAuditlogImpl.clear(); - rh.executeGetRequest("/_opendistro/_security/api/internalusers"); - Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertFalse(AuditMessage.BCRYPT_HASH.matcher(TestAuditlogImpl.sb.toString()).matches()); + final AuditMessage message1 = TestAuditlogImpl.doThenWaitForMessage(() -> { + rh.executeGetRequest("/_opendistro/_security/api/internalusers"); + }); + + Assert.assertFalse(AuditMessage.BCRYPT_HASH.matcher(message1.toString()).matches()); // read internal user worf and verify no BCrypt hash is present in audit logs - TestAuditlogImpl.clear(); - rh.executeGetRequest("/_opendistro/_security/api/internalusers/worf"); - Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertFalse(AuditMessage.BCRYPT_HASH.matcher(TestAuditlogImpl.sb.toString()).matches()); + final AuditMessage message2 = TestAuditlogImpl.doThenWaitForMessage(() -> { + rh.executeGetRequest("/_opendistro/_security/api/internalusers/worf"); + Assert.assertFalse(AuditMessage.BCRYPT_HASH.matcher(TestAuditlogImpl.sb.toString()).matches()); + }); + + Assert.assertFalse(AuditMessage.BCRYPT_HASH.matcher(message2.toString()).matches()); // create internal user and verify no BCrypt hash is present in audit logs - TestAuditlogImpl.clear(); - rh.executePutRequest("/_opendistro/_security/api/internalusers/test", "{ \"password\":\"some new user password\"}"); - Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertFalse(AuditMessage.BCRYPT_HASH.matcher(TestAuditlogImpl.sb.toString()).matches()); + final AuditMessage message3 = TestAuditlogImpl.doThenWaitForMessage(() -> { + rh.executePutRequest("/_opendistro/_security/api/internalusers/test", "{ \"password\":\"some new user password\"}"); + }); + + Assert.assertFalse(AuditMessage.BCRYPT_HASH.matcher(message3.toString()).matches()); + } + + private List setupAndReturnAuditMessages(Settings settings) { + // When OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED is set to true, there is: + // - 1 message with COMPLIANCE_INTERNAL_CONFIG_WRITE as category. + // - 1 message with COMPLIANCE_EXTERNAL_CONFIG as category for each node. + int numNodes = ClusterConfiguration.DEFAULT.getNodes(); + boolean externalConfigEnabled = settings.getAsBoolean( + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, + false + ); + int expectedMessageCount = externalConfigEnabled ? (numNodes + 1) : 1; + final List messages = TestAuditlogImpl.doThenWaitForMessages(() -> { + try { + setup(settings); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + }, expectedMessageCount); + int complianceInternalConfigWriteCount = 0; + int complianceExternalConfigCount = 0; + for (AuditMessage message : messages) { + if (AuditCategory.COMPLIANCE_INTERNAL_CONFIG_WRITE.equals(message.getCategory())) { + complianceInternalConfigWriteCount++; + } else if (AuditCategory.COMPLIANCE_EXTERNAL_CONFIG.equals(message.getCategory())) { + complianceExternalConfigCount++; + } + } + assertThat(complianceInternalConfigWriteCount, equalTo(1)); + if (externalConfigEnabled) { + assertThat(complianceExternalConfigCount, equalTo(numNodes)); + } + return messages; } } diff --git a/src/test/java/org/opensearch/security/auditlog/config/AuditConfigFilterTest.java b/src/test/java/org/opensearch/security/auditlog/config/AuditConfigFilterTest.java index e40e65549f..a28d940862 100644 --- a/src/test/java/org/opensearch/security/auditlog/config/AuditConfigFilterTest.java +++ b/src/test/java/org/opensearch/security/auditlog/config/AuditConfigFilterTest.java @@ -57,6 +57,7 @@ public void testDefault() { assertTrue(auditConfigFilter.shouldExcludeSensitiveHeaders()); assertSame(WildcardMatcher.NONE, auditConfigFilter.getIgnoredAuditRequestsMatcher()); assertEquals(defaultIgnoredUserMatcher, auditConfigFilter.getIgnoredAuditUsersMatcher()); + assertSame(WildcardMatcher.NONE, auditConfigFilter.getIgnoredCustomHeadersMatcher()); assertEquals(auditConfigFilter.getDisabledRestCategories(), defaultDisabledCategories); assertEquals(auditConfigFilter.getDisabledTransportCategories(), defaultDisabledCategories); } @@ -73,6 +74,7 @@ public void testConfig() { .put(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS, false) .putList(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS, "test-request") .putList(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS, "test-user") + .putList(ConfigConstants.SECURITY_AUDIT_IGNORE_HEADERS, "test-header") .putList( ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, BAD_HEADERS.toString(), @@ -95,6 +97,7 @@ public void testConfig() { assertFalse(auditConfigFilter.shouldExcludeSensitiveHeaders()); assertEquals(WildcardMatcher.from(Collections.singleton("test-user")), auditConfigFilter.getIgnoredAuditUsersMatcher()); assertEquals(WildcardMatcher.from(Collections.singleton("test-request")), auditConfigFilter.getIgnoredAuditRequestsMatcher()); + assertEquals(WildcardMatcher.from(Collections.singleton("test-header")), auditConfigFilter.getIgnoredCustomHeadersMatcher()); assertEquals(auditConfigFilter.getDisabledRestCategories(), EnumSet.of(BAD_HEADERS, SSL_EXCEPTION)); assertEquals(auditConfigFilter.getDisabledTransportCategories(), EnumSet.of(FAILED_LOGIN, MISSING_PRIVILEGES)); } @@ -121,6 +124,7 @@ public void testEmpty() { final Settings settings = Settings.builder() .putList(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS, Collections.emptyList()) .putList(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS, Collections.emptyList()) + .putList(ConfigConstants.SECURITY_AUDIT_IGNORE_HEADERS, Collections.emptyList()) .putList(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, Collections.emptyList()) .putList(ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES, Collections.emptyList()) .build(); diff --git a/src/test/java/org/opensearch/security/auditlog/config/AuditConfigSerializeTest.java b/src/test/java/org/opensearch/security/auditlog/config/AuditConfigSerializeTest.java index 0b50c2ac20..52cb39f41e 100644 --- a/src/test/java/org/opensearch/security/auditlog/config/AuditConfigSerializeTest.java +++ b/src/test/java/org/opensearch/security/auditlog/config/AuditConfigSerializeTest.java @@ -72,6 +72,8 @@ public void testDefaultSerialize() throws IOException { .field("exclude_sensitive_headers", true) .field("ignore_users", Collections.singletonList("kibanaserver")) .field("ignore_requests", Collections.emptyList()) + .field("ignore_headers", Collections.emptyList()) + .field("ignore_url_params", Collections.emptyList()) .endObject() .startObject("compliance") .field("enabled", true) @@ -107,6 +109,7 @@ public void testDefaultDeserialize() throws IOException { assertTrue(audit.shouldExcludeSensitiveHeaders()); assertSame(WildcardMatcher.NONE, audit.getIgnoredAuditRequestsMatcher()); assertEquals(DEFAULT_IGNORED_USER, audit.getIgnoredAuditUsersMatcher()); + assertEquals(WildcardMatcher.NONE, audit.getIgnoredCustomHeadersMatcher()); assertFalse(compliance.shouldLogExternalConfig()); assertFalse(compliance.shouldLogInternalConfig()); assertFalse(compliance.shouldLogReadMetadataOnly()); @@ -133,6 +136,8 @@ public void testDeserialize() throws IOException { .field("exclude_sensitive_headers", true) .field("ignore_users", Collections.singletonList("test-user-1")) .field("ignore_requests", Collections.singletonList("test-request")) + .field("ignore_headers", Collections.singletonList("test-headers")) + .field("ignore_url_params", Collections.singletonList("test-param")) .endObject() .startObject("compliance") .field("enabled", true) @@ -196,6 +201,8 @@ public void testSerialize() throws IOException { true, ImmutableSet.of("ignore-user-1", "ignore-user-2"), ImmutableSet.of("ignore-request-1"), + ImmutableSet.of("test-header"), + ImmutableSet.of("test-param"), EnumSet.of(AuditCategory.FAILED_LOGIN, AuditCategory.GRANTED_PRIVILEGES), EnumSet.of(AUTHENTICATED) ); @@ -210,6 +217,7 @@ public void testSerialize() throws IOException { false, Collections.singletonList("test-write-watch-index"), Collections.singleton("test-user-2"), + null, Settings.EMPTY ); final AuditConfig auditConfig = new AuditConfig(true, audit, compliance); @@ -227,6 +235,8 @@ public void testSerialize() throws IOException { .field("exclude_sensitive_headers", true) .field("ignore_users", ImmutableList.of("ignore-user-1", "ignore-user-2")) .field("ignore_requests", Collections.singletonList("ignore-request-1")) + .field("ignore_headers", Collections.singletonList("test-header")) + .field("ignore_url_params", Collections.singletonList("test-param")) .endObject() .startObject("compliance") .field("enabled", true) @@ -269,6 +279,8 @@ public void testNullSerialize() throws IOException { .field("exclude_sensitive_headers", true) .field("ignore_users", ImmutableList.of("kibanaserver")) .field("ignore_requests", Collections.emptyList()) + .field("ignore_headers", Collections.emptyList()) + .field("ignore_url_params", Collections.emptyList()) .endObject() .startObject("compliance") .field("enabled", true) @@ -287,6 +299,7 @@ public void testNullSerialize() throws IOException { // act final String json = objectMapper.writeValueAsString(auditConfig); // assert + assertTrue(compareJson(jsonBuilder.toString(), json)); } diff --git a/src/test/java/org/opensearch/security/auditlog/impl/AuditMessageTest.java b/src/test/java/org/opensearch/security/auditlog/impl/AuditMessageTest.java index d915c02e55..08e4c2ea61 100644 --- a/src/test/java/org/opensearch/security/auditlog/impl/AuditMessageTest.java +++ b/src/test/java/org/opensearch/security/auditlog/impl/AuditMessageTest.java @@ -26,8 +26,16 @@ import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.collect.Tuple; import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.bytes.BytesArray; import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.http.HttpChannel; +import org.opensearch.http.HttpRequest; +import org.opensearch.rest.RestRequest; import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.auditlog.config.AuditConfig; +import org.opensearch.security.filter.SecurityRequest; +import org.opensearch.security.filter.SecurityRequestFactory; import org.opensearch.security.securityconf.impl.CType; import static org.junit.Assert.assertEquals; @@ -60,32 +68,45 @@ public class AuditMessageTest { ); private AuditMessage message; + private AuditConfig auditConfig; @Before public void setUp() { final ClusterService clusterServiceMock = mock(ClusterService.class); when(clusterServiceMock.localNode()).thenReturn(mock(DiscoveryNode.class)); when(clusterServiceMock.getClusterName()).thenReturn(mock(ClusterName.class)); + auditConfig = mock(AuditConfig.class); + final AuditConfig.Filter auditFilter = mock(AuditConfig.Filter.class); + when(auditConfig.getFilter()).thenReturn(auditFilter); message = new AuditMessage(AuditCategory.AUTHENTICATED, clusterServiceMock, AuditLog.Origin.REST, AuditLog.Origin.REST); } @Test - public void testRestHeadersAreFiltered() { - message.addRestHeaders(TEST_REST_HEADERS, true); + public void testAuthorizationRestHeadersAreFiltered() { + when(auditConfig.getFilter().shouldExcludeHeader("test-header")).thenReturn(false); + message.addRestHeaders(TEST_REST_HEADERS, true, auditConfig.getFilter()); assertEquals(message.getAsMap().get(AuditMessage.REST_REQUEST_HEADERS), ImmutableMap.of("test-header", ImmutableList.of("test-4"))); } + @Test + public void testCustomRestHeadersAreFiltered() { + when(auditConfig.getFilter().shouldExcludeHeader("test-header")).thenReturn(true); + message.addRestHeaders(TEST_REST_HEADERS, true, auditConfig.getFilter()); + assertEquals(message.getAsMap().get(AuditMessage.REST_REQUEST_HEADERS), Map.of()); + } + @Test public void testRestHeadersNull() { - message.addRestHeaders(null, true); + message.addRestHeaders(null, true, null); assertNull(message.getAsMap().get(AuditMessage.REST_REQUEST_HEADERS)); - message.addRestHeaders(Collections.emptyMap(), true); + message.addRestHeaders(Collections.emptyMap(), true, null); assertNull(message.getAsMap().get(AuditMessage.REST_REQUEST_HEADERS)); } @Test public void testRestHeadersAreNotFiltered() { - message.addRestHeaders(TEST_REST_HEADERS, false); + when(auditConfig.getFilter().shouldExcludeHeader("test-header")).thenReturn(false); + message.addRestHeaders(TEST_REST_HEADERS, false, null); assertEquals(message.getAsMap().get(AuditMessage.REST_REQUEST_HEADERS), TEST_REST_HEADERS); } @@ -141,4 +162,41 @@ public void testBCryptHashIsRedacted() { message.addSecurityConfigTupleToRequestBody(new Tuple<>(XContentType.JSON, ref), internalUsersDocId); assertEquals("Hash in tuple is __HASH__", message.getAsMap().get(AuditMessage.REQUEST_BODY)); } + + @Test + public void testRequestBodyLoggingWithInvalidSourceOrContentTypeParam() { + when(auditConfig.getFilter().shouldLogRequestBody()).thenReturn(true); + + HttpRequest httpRequest = mock(HttpRequest.class); + + // No content or Source paramater + when(httpRequest.uri()).thenReturn(""); + when(httpRequest.content()).thenReturn(new BytesArray(new byte[0])); + + RestRequest restRequest = RestRequest.request(mock(NamedXContentRegistry.class), httpRequest, mock(HttpChannel.class)); + SecurityRequest request = SecurityRequestFactory.from(restRequest); + + message.addRestRequestInfo(request, auditConfig.getFilter()); + assertNull(message.getAsMap().get(AuditMessage.REQUEST_BODY)); + + // No source parameter, content present but Invalid content-type header + when(httpRequest.uri()).thenReturn(""); + when(httpRequest.content()).thenReturn(new BytesArray(new byte[1])); + + restRequest = RestRequest.request(mock(NamedXContentRegistry.class), httpRequest, mock(HttpChannel.class)); + request = SecurityRequestFactory.from(restRequest); + + message.addRestRequestInfo(request, auditConfig.getFilter()); + assertEquals("ERROR: Unable to generate request body", message.getAsMap().get(AuditMessage.REQUEST_BODY)); + + // No content, source parameter present but Invalid source-content-type parameter + when(httpRequest.uri()).thenReturn("/aaaa?source=request_body"); + when(httpRequest.content()).thenReturn(new BytesArray(new byte[0])); + + restRequest = RestRequest.request(mock(NamedXContentRegistry.class), httpRequest, mock(HttpChannel.class)); + request = SecurityRequestFactory.from(restRequest); + + message.addRestRequestInfo(request, auditConfig.getFilter()); + assertEquals("ERROR: Unable to generate request body", message.getAsMap().get(AuditMessage.REQUEST_BODY)); + } } diff --git a/src/test/java/org/opensearch/security/auditlog/impl/TracingTests.java b/src/test/java/org/opensearch/security/auditlog/impl/TracingTests.java index 15728537e2..796c73b811 100644 --- a/src/test/java/org/opensearch/security/auditlog/impl/TracingTests.java +++ b/src/test/java/org/opensearch/security/auditlog/impl/TracingTests.java @@ -11,7 +11,7 @@ package org.opensearch.security.auditlog.impl; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/auditlog/integration/BasicAuditlogTest.java b/src/test/java/org/opensearch/security/auditlog/integration/BasicAuditlogTest.java index 6c1812c32b..c4784d14b8 100644 --- a/src/test/java/org/opensearch/security/auditlog/integration/BasicAuditlogTest.java +++ b/src/test/java/org/opensearch/security/auditlog/integration/BasicAuditlogTest.java @@ -16,8 +16,8 @@ import com.google.common.collect.ImmutableMap; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -149,7 +149,7 @@ public void testSSLPlainText() throws Exception { Assert.assertEquals(AuditCategory.SSL_EXCEPTION, message.getCategory()); Assert.assertTrue(message.getExceptionStackTrace().contains("not an SSL/TLS record")); }); - Assert.assertTrue(validateMsgs(messages)); + validateMsgs(messages); } @Test @@ -182,7 +182,7 @@ public void testTaskId() throws Exception { TestAuditlogImpl.messages.get(1).getAsMap().get(AuditMessage.TASK_ID), TestAuditlogImpl.messages.get(1).getAsMap().get(AuditMessage.TASK_ID) ); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test @@ -211,7 +211,7 @@ public void testDefaultsRest() throws Exception { Assert.assertTrue(auditLogImpl.contains("\"audit_request_effective_user\" : \"admin\"")); Assert.assertTrue(auditLogImpl.contains("REST")); Assert.assertFalse(auditLogImpl.toLowerCase().contains("authorization")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test @@ -317,7 +317,7 @@ public void testWrongUser() throws Exception { Assert.assertTrue(TestAuditlogImpl.sb.toString(), TestAuditlogImpl.sb.toString().contains(AuditMessage.UTC_TIMESTAMP)); Assert.assertFalse(TestAuditlogImpl.sb.toString(), TestAuditlogImpl.sb.toString().contains("AUTHENTICATED")); Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testUnknownAuthorization() throws Exception { @@ -329,7 +329,7 @@ public void testUnknownAuthorization() throws Exception { Assert.assertTrue(TestAuditlogImpl.sb.toString().contains(AuditMessage.UTC_TIMESTAMP)); Assert.assertFalse(TestAuditlogImpl.sb.toString().contains("AUTHENTICATED")); Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testUnauthenticated() throws Exception { @@ -345,7 +345,7 @@ public void testUnauthenticated() throws Exception { Assert.assertTrue(auditLogImpl.contains("/_search")); Assert.assertTrue(auditLogImpl.contains(AuditMessage.UTC_TIMESTAMP)); Assert.assertFalse(auditLogImpl.contains("AUTHENTICATED")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @@ -353,7 +353,7 @@ public void testJustAuthenticated() throws Exception { HttpResponse response = rh.executeGetRequest("", encodeBasicHeader("admin", "admin")); Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); Assert.assertEquals(0, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testSecurityIndexAttempt() throws Exception { @@ -366,7 +366,7 @@ public void testSecurityIndexAttempt() throws Exception { Assert.assertTrue(TestAuditlogImpl.sb.toString().contains(AuditMessage.UTC_TIMESTAMP)); Assert.assertFalse(TestAuditlogImpl.sb.toString().contains("AUTHENTICATED")); Assert.assertEquals(2, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testBadHeader() throws Exception { @@ -381,7 +381,7 @@ public void testBadHeader() throws Exception { Assert.assertTrue(TestAuditlogImpl.sb.toString(), TestAuditlogImpl.sb.toString().contains("BAD_HEADERS")); Assert.assertTrue(TestAuditlogImpl.sb.toString(), TestAuditlogImpl.sb.toString().contains("_opendistro_security_bad")); Assert.assertEquals(TestAuditlogImpl.sb.toString(), 1, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testMissingPriv() throws Exception { @@ -395,7 +395,7 @@ public void testMissingPriv() throws Exception { Assert.assertTrue(TestAuditlogImpl.sb.toString().contains(AuditMessage.UTC_TIMESTAMP)); Assert.assertFalse(TestAuditlogImpl.sb.toString().contains("AUTHENTICATED")); Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testMsearch() throws Exception { @@ -419,7 +419,7 @@ public void testMsearch() throws Exception { Assert.assertTrue(auditLogImpl.contains("audit_trace_task_id")); Assert.assertEquals(auditLogImpl, 4, TestAuditlogImpl.messages.size()); Assert.assertFalse(auditLogImpl.toLowerCase().contains("authorization")); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testBulkAuth() throws Exception { @@ -458,7 +458,7 @@ public void testBulkAuth() throws Exception { Assert.assertTrue(auditLogImpl.contains("audit_trace_task_id")); // may vary because we log shardrequests which are not predictable here Assert.assertTrue(TestAuditlogImpl.messages.size() >= 17); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testBulkNonAuth() throws Exception { @@ -496,7 +496,7 @@ public void testBulkNonAuth() throws Exception { Assert.assertTrue(auditLogImpl.contains("IndexRequest")); // may vary because we log shardrequests which are not predictable here Assert.assertTrue(TestAuditlogImpl.messages.size() >= 7); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } public void testUpdateSettings() throws Exception { @@ -521,7 +521,7 @@ public void testUpdateSettings() throws Exception { Assert.assertTrue(auditLogImpl.contains(expectedRequestBodyLog)); // may vary because we log may hit cluster manager directly or not Assert.assertTrue(TestAuditlogImpl.messages.size() > 1); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test @@ -620,7 +620,7 @@ public void testAliases() throws Exception { Assert.assertTrue(auditLogImpl.contains("starfleet")); Assert.assertTrue(auditLogImpl.contains("sf")); Assert.assertEquals(2, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test @@ -682,7 +682,7 @@ public void testScroll() throws Exception { Assert.assertTrue(auditLogImpl.contains("InternalScrollSearchRequest")); Assert.assertTrue(auditLogImpl.contains("MISSING_PRIVILEGES")); Assert.assertTrue(TestAuditlogImpl.messages.size() > 2); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test @@ -718,7 +718,7 @@ public void testAliasResolution() throws Exception { Assert.assertTrue(auditLogImpl.contains("audit_trace_resolved_indices")); Assert.assertTrue(auditLogImpl.contains("vulcangov")); Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); TestAuditlogImpl.clear(); } @@ -747,7 +747,7 @@ public void testAliasBadHeaders() throws Exception { Assert.assertTrue(auditLogImpl.contains("BAD_HEADERS")); Assert.assertTrue(auditLogImpl.contains("xxx")); Assert.assertEquals(1, TestAuditlogImpl.messages.size()); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); TestAuditlogImpl.clear(); } @@ -780,7 +780,7 @@ public void testIndexCloseDelete() throws Exception { Assert.assertTrue(auditLogImpl.contains("indices:admin/close")); Assert.assertTrue(auditLogImpl.contains("indices:admin/delete")); Assert.assertTrue(auditLogImpl, TestAuditlogImpl.messages.size() >= 2); - Assert.assertTrue(validateMsgs(TestAuditlogImpl.messages)); + validateMsgs(TestAuditlogImpl.messages); } @Test diff --git a/src/test/java/org/opensearch/security/auditlog/integration/SSLAuditlogTest.java b/src/test/java/org/opensearch/security/auditlog/integration/SSLAuditlogTest.java index 82c56d4b23..0b92c952f6 100644 --- a/src/test/java/org/opensearch/security/auditlog/integration/SSLAuditlogTest.java +++ b/src/test/java/org/opensearch/security/auditlog/integration/SSLAuditlogTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.auditlog.integration; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.After; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/auditlog/sink/SinkProviderTest.java b/src/test/java/org/opensearch/security/auditlog/sink/SinkProviderTest.java index 5e3203261f..af8204a5c7 100644 --- a/src/test/java/org/opensearch/security/auditlog/sink/SinkProviderTest.java +++ b/src/test/java/org/opensearch/security/auditlog/sink/SinkProviderTest.java @@ -88,6 +88,12 @@ public void testConfiguration() throws Exception { Assert.assertEquals("loggername", lsink.loggerName); Assert.assertEquals(Level.DEBUG, lsink.logLevel); + sink = provider.getSink("endpoint13"); + Assert.assertEquals(Log4JSink.class, sink.getClass()); + lsink = (Log4JSink) sink; + Assert.assertEquals("audit", lsink.loggerName); + Assert.assertEquals(Level.INFO, lsink.logLevel); + } @Test diff --git a/src/test/java/org/opensearch/security/auth/limiting/HeapBasedRateTrackerTest.java b/src/test/java/org/opensearch/security/auth/limiting/HeapBasedRateTrackerTest.java index c92c328564..aaae27e8c3 100644 --- a/src/test/java/org/opensearch/security/auth/limiting/HeapBasedRateTrackerTest.java +++ b/src/test/java/org/opensearch/security/auth/limiting/HeapBasedRateTrackerTest.java @@ -17,7 +17,9 @@ package org.opensearch.security.auth.limiting; -import org.junit.Ignore; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.LongSupplier; + import org.junit.Test; import org.opensearch.security.util.ratetracking.HeapBasedRateTracker; @@ -27,9 +29,12 @@ public class HeapBasedRateTrackerTest { + private final AtomicLong currentTime = new AtomicLong(1); + private LongSupplier timeProvider = () -> currentTime.getAndAdd(1); + @Test public void simpleTest() throws Exception { - HeapBasedRateTracker tracker = new HeapBasedRateTracker<>(100, 5, 100_000); + HeapBasedRateTracker tracker = new HeapBasedRateTracker<>(100, 5, 100_000, timeProvider); assertFalse(tracker.track("a")); assertFalse(tracker.track("a")); @@ -40,9 +45,8 @@ public void simpleTest() throws Exception { } @Test - @Ignore // https://github.com/opensearch-project/security/issues/2193 public void expiryTest() throws Exception { - HeapBasedRateTracker tracker = new HeapBasedRateTracker<>(100, 5, 100_000); + HeapBasedRateTracker tracker = new HeapBasedRateTracker<>(100, 5, 100_000, timeProvider); assertFalse(tracker.track("a")); assertFalse(tracker.track("a")); @@ -58,20 +62,20 @@ public void expiryTest() throws Exception { assertFalse(tracker.track("c")); - Thread.sleep(50); + currentTime.addAndGet(50); assertFalse(tracker.track("c")); assertFalse(tracker.track("c")); assertFalse(tracker.track("c")); - Thread.sleep(55); + currentTime.addAndGet(55); assertFalse(tracker.track("c")); assertTrue(tracker.track("c")); assertFalse(tracker.track("a")); - Thread.sleep(55); + currentTime.addAndGet(55); assertFalse(tracker.track("c")); assertFalse(tracker.track("c")); assertTrue(tracker.track("c")); @@ -79,21 +83,20 @@ public void expiryTest() throws Exception { } @Test - @Ignore // https://github.com/opensearch-project/security/issues/2193 public void maxTwoTriesTest() throws Exception { - HeapBasedRateTracker tracker = new HeapBasedRateTracker<>(100, 2, 100_000); + HeapBasedRateTracker tracker = new HeapBasedRateTracker<>(100, 2, 100_000, timeProvider); assertFalse(tracker.track("a")); assertTrue(tracker.track("a")); assertFalse(tracker.track("b")); - Thread.sleep(50); + currentTime.addAndGet(50); assertTrue(tracker.track("b")); - Thread.sleep(55); + currentTime.addAndGet(55); assertTrue(tracker.track("b")); - Thread.sleep(105); + currentTime.addAndGet(105); assertFalse(tracker.track("b")); assertTrue(tracker.track("b")); diff --git a/src/test/java/org/opensearch/security/cache/CachingTest.java b/src/test/java/org/opensearch/security/cache/CachingTest.java index 39f0e1315d..cb71be78e1 100644 --- a/src/test/java/org/opensearch/security/cache/CachingTest.java +++ b/src/test/java/org/opensearch/security/cache/CachingTest.java @@ -11,8 +11,8 @@ package org.opensearch.security.cache; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java b/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java index e2dd28b563..0bf9e0e9df 100644 --- a/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java +++ b/src/test/java/org/opensearch/security/ccstest/CrossClusterSearchTests.java @@ -27,7 +27,7 @@ package org.opensearch.security.ccstest; import com.google.common.collect.Lists; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.After; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java b/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java index ea329b7b2c..15fe91d822 100644 --- a/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java +++ b/src/test/java/org/opensearch/security/ccstest/RemoteReindexTests.java @@ -26,7 +26,7 @@ package org.opensearch.security.ccstest; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.After; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/configuration/ConfigurationRepositoryTest.java b/src/test/java/org/opensearch/security/configuration/ConfigurationRepositoryTest.java new file mode 100644 index 0000000000..30cbbe6a01 --- /dev/null +++ b/src/test/java/org/opensearch/security/configuration/ConfigurationRepositoryTest.java @@ -0,0 +1,292 @@ +/* + * 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.configuration; + +import java.io.IOException; +import java.nio.file.Path; +import java.time.Instant; +import java.util.Map; +import java.util.Set; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.client.Client; +import org.opensearch.cluster.ClusterChangedEvent; +import org.opensearch.cluster.ClusterState; +import org.opensearch.cluster.ClusterStateUpdateTask; +import org.opensearch.cluster.metadata.Metadata; +import org.opensearch.cluster.node.DiscoveryNodes; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.Priority; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.auditlog.AuditLog; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.state.SecurityConfig; +import org.opensearch.security.state.SecurityMetadata; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.SecurityIndexHandler; +import org.opensearch.security.transport.SecurityInterceptorTests; +import org.opensearch.threadpool.ThreadPool; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.opensearch.security.support.ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX; +import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doCallRealMethod; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class ConfigurationRepositoryTest { + + @Mock + private Client localClient; + @Mock + private AuditLog auditLog; + @Mock + private Path path; + @Mock + private ClusterService clusterService; + + private ThreadPool threadPool; + + @Mock + private SecurityIndexHandler securityIndexHandler; + + @Mock + private ClusterChangedEvent event; + + @Before + public void setUp() { + Settings settings = Settings.builder() + .put("node.name", SecurityInterceptorTests.class.getSimpleName()) + .put("request.headers.default", "1") + .build(); + + threadPool = new ThreadPool(settings); + + final var previousState = mock(ClusterState.class); + final var previousDiscoveryNodes = mock(DiscoveryNodes.class); + when(previousState.nodes()).thenReturn(previousDiscoveryNodes); + when(event.previousState()).thenReturn(previousState); + + final var newState = mock(ClusterState.class); + when(event.state()).thenReturn(newState); + when(event.state().metadata()).thenReturn(mock(Metadata.class)); + + when(event.state().custom(SecurityMetadata.TYPE)).thenReturn(null); + } + + private ConfigurationRepository createConfigurationRepository(Settings settings) { + return new ConfigurationRepository( + settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX), + settings, + path, + threadPool, + localClient, + clusterService, + auditLog, + securityIndexHandler + ); + } + + @Test + public void create_shouldReturnConfigurationRepository() { + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + + assertThat(configRepository, is(notNullValue())); + assertThat(configRepository, instanceOf(ConfigurationRepository.class)); + } + + @Test + public void initOnNodeStart_withSecurityIndexCreationEnabledShouldSetInstallDefaultConfigTrue() { + Settings settings = Settings.builder().put(SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, true).build(); + + ConfigurationRepository configRepository = createConfigurationRepository(settings); + + final var result = configRepository.initOnNodeStart(); + + assertThat(result.join(), is(true)); + } + + @Test + public void initOnNodeStart_withSecurityIndexNotCreatedShouldNotSetInstallDefaultConfig() { + Settings settings = Settings.builder().put(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, false).build(); + + ConfigurationRepository configRepository = createConfigurationRepository(settings); + + final var result = configRepository.initOnNodeStart(); + + assertThat(result.join(), is(false)); + } + + @Test + public void getConfiguration_withInvalidConfigurationShouldReturnNewEmptyConfigurationObject() throws IOException { + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + + SecurityDynamicConfiguration config = configRepository.getConfiguration(CType.CONFIG); + SecurityDynamicConfiguration emptyConfig = SecurityDynamicConfiguration.empty(); + + assertThat(config, is(instanceOf(SecurityDynamicConfiguration.class))); + assertThat(config.getCEntries().size(), is(equalTo(0))); + assertThat(config.getVersion(), is(equalTo(emptyConfig.getVersion()))); + assertThat(config.getCType(), is(equalTo(emptyConfig.getCType()))); + assertThat(config.getSeqNo(), is(equalTo(emptyConfig.getSeqNo()))); + assertThat(config, is(not(equalTo(emptyConfig)))); + } + + @Test + public void testClusterChanged_shouldInitSecurityIndexIfNoSecurityData() { + when(event.previousState().nodes().isLocalNodeElectedClusterManager()).thenReturn(false); + when(event.localNodeClusterManager()).thenReturn(true); + + final var configurationRepository = mock(ConfigurationRepository.class); + doCallRealMethod().when(configurationRepository).clusterChanged(any()); + configurationRepository.clusterChanged(event); + + verify(configurationRepository).initSecurityIndex(any()); + } + + @Test + public void testClusterChanged_shouldExecuteInitialization() { + when(event.state().custom(SecurityMetadata.TYPE)).thenReturn(new SecurityMetadata(Instant.now(), Set.of())); + + final var configurationRepository = mock(ConfigurationRepository.class); + doCallRealMethod().when(configurationRepository).clusterChanged(any()); + configurationRepository.clusterChanged(event); + + verify(configurationRepository).executeConfigurationInitialization(any()); + } + + @Test + public void testClusterChanged_shouldNotExecuteInitialization() { + final var configurationRepository = mock(ConfigurationRepository.class); + doCallRealMethod().when(configurationRepository).clusterChanged(any()); + configurationRepository.clusterChanged(event); + + verify(configurationRepository, never()).executeConfigurationInitialization(any()); + } + + @Test + public void testInitSecurityIndex_shouldCreateIndexAndUploadConfiguration() throws Exception { + System.setProperty("security.default_init.dir", Path.of(".").toString()); + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener) invocation.getArgument(0); + listener.onResponse(true); + return null; + }).when(securityIndexHandler).createIndex(any()); + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener>) invocation.getArgument(1); + listener.onResponse(Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))); + return null; + }).when(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + when(event.state().metadata().hasIndex(OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX)).thenReturn(false); + configRepository.initSecurityIndex(event); + + final var clusterStateUpdateTaskCaptor = ArgumentCaptor.forClass(ClusterStateUpdateTask.class); + verify(securityIndexHandler).createIndex(any()); + verify(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + verify(clusterService).submitStateUpdateTask(anyString(), clusterStateUpdateTaskCaptor.capture()); + verifyNoMoreInteractions(clusterService, securityIndexHandler); + + assertClusterState(clusterStateUpdateTaskCaptor); + } + + @Test + public void testInitSecurityIndex_shouldUploadConfigIfIndexCreated() throws Exception { + System.setProperty("security.default_init.dir", Path.of(".").toString()); + + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener>) invocation.getArgument(1); + listener.onResponse(Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))); + return null; + }).when(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + + when(event.state().metadata().hasIndex(OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX)).thenReturn(true); + + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + configRepository.initSecurityIndex(event); + + final var clusterStateUpdateTaskCaptor = ArgumentCaptor.forClass(ClusterStateUpdateTask.class); + + verify(event.state().metadata()).hasIndex(OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + verify(clusterService).submitStateUpdateTask(anyString(), clusterStateUpdateTaskCaptor.capture()); + verify(securityIndexHandler, never()).createIndex(any()); + verify(securityIndexHandler).uploadDefaultConfiguration(any(), any()); + verifyNoMoreInteractions(securityIndexHandler, clusterService); + + assertClusterState(clusterStateUpdateTaskCaptor); + } + + @Test + public void testExecuteConfigurationInitialization_executeInitializationOnlyOnce() throws Exception { + doAnswer(invocation -> { + @SuppressWarnings("unchecked") + final var listener = (ActionListener>>) invocation.getArgument(1); + listener.onResponse(Map.of()); + return null; + }).when(securityIndexHandler).loadConfiguration(any(), any()); + + ConfigurationRepository configRepository = createConfigurationRepository(Settings.EMPTY); + configRepository.executeConfigurationInitialization( + new SecurityMetadata(Instant.now(), Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))) + ).get(); + + verify(securityIndexHandler).loadConfiguration(any(), any()); + verifyNoMoreInteractions(securityIndexHandler); + + reset(securityIndexHandler); + + configRepository.executeConfigurationInitialization( + new SecurityMetadata(Instant.now(), Set.of(new SecurityConfig(CType.CONFIG, "aaa", null))) + ).get(); + + verify(securityIndexHandler, never()).loadConfiguration(any(), any()); + verifyNoMoreInteractions(securityIndexHandler); + } + + void assertClusterState(final ArgumentCaptor clusterStateUpdateTaskCaptor) throws Exception { + final var initializedStateUpdate = clusterStateUpdateTaskCaptor.getValue(); + assertEquals(Priority.IMMEDIATE, initializedStateUpdate.priority()); + var clusterState = initializedStateUpdate.execute(ClusterState.EMPTY_STATE); + SecurityMetadata securityMetadata = clusterState.custom(SecurityMetadata.TYPE); + assertNotNull(securityMetadata.created()); + assertNotNull(securityMetadata.configuration()); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedComplexMappingTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedComplexMappingTest.java index 3a03e8add4..8490b42f12 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedComplexMappingTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedComplexMappingTest.java @@ -13,7 +13,7 @@ import java.nio.charset.StandardCharsets; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedTest.java index 672eb2abb0..226574c588 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/CustomFieldMaskedTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DateMathTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DateMathTest.java index 43b78e9803..db15602867 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DateMathTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DateMathTest.java @@ -15,7 +15,7 @@ import java.util.Date; import java.util.TimeZone; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsDateMathTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsDateMathTest.java index bc349ace37..87d2ea6b52 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsDateMathTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsDateMathTest.java @@ -15,7 +15,7 @@ import java.time.ZoneId; import java.time.format.DateTimeFormatter; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java index 6ac4690a70..ad2b2433cf 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsFlsCrossClusterSearchTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.After; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsNestedTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsNestedTest.java index 67abf5f61b..36e7ec0905 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsNestedTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsNestedTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsPropsReplaceTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsPropsReplaceTest.java index 5fe6419a02..88ebdbe36c 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsPropsReplaceTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsPropsReplaceTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsScrollTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsScrollTest.java index cc7b9e305d..0662c65109 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsScrollTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsScrollTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java index 587f759315..e4dffcc31f 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/DlsTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FieldMaskedTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FieldMaskedTest.java index 2628bebbc0..e18eae5780 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FieldMaskedTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FieldMaskedTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/Fls983Test.java b/src/test/java/org/opensearch/security/dlic/dlsfls/Fls983Test.java index c486599ea4..c17b5c9f0c 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/Fls983Test.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/Fls983Test.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestAB.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestAB.java index 5e7584a1bc..33b8296814 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestAB.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestAB.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestForbiddenField.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestForbiddenField.java index a3776e567c..fd164802d3 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestForbiddenField.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestForbiddenField.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestMulti.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestMulti.java index e9d32f18ea..5cc9f7423a 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestMulti.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsDlsTestMulti.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsExistsFieldsTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsExistsFieldsTest.java index bc3d306627..b58b80368a 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsExistsFieldsTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsExistsFieldsTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsTest.java index a910cf5663..5681479085 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsTest.java @@ -13,7 +13,7 @@ import java.io.IOException; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsWcTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsWcTest.java index f6cfd036fd..2c3235cf27 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsWcTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFieldsWcTest.java @@ -13,7 +13,7 @@ import java.io.IOException; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFlatTests.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFlatTests.java index 7899d3c2e5..3441108395 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFlatTests.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsFlatTests.java @@ -15,7 +15,7 @@ import java.util.function.Consumer; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.action.admin.indices.create.CreateIndexRequest; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsIndexingTests.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsIndexingTests.java index 2d7ed0efcf..2552fcdbc3 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsIndexingTests.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsIndexingTests.java @@ -12,7 +12,7 @@ package org.opensearch.security.dlic.dlsfls; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.action.index.IndexRequest; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsKeywordTests.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsKeywordTests.java index 1c51ec99b7..8117b7e0ba 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsKeywordTests.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsKeywordTests.java @@ -14,7 +14,7 @@ import java.util.Arrays; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.action.index.IndexRequest; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsPerfTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsPerfTest.java index 32092cc8ed..81553662fd 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsPerfTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsPerfTest.java @@ -15,7 +15,7 @@ import java.util.HashMap; import java.util.Map; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsTest.java index 66c962051f..a2787af61c 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/FlsTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/FlsTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/IndexPatternTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/IndexPatternTest.java index 75eb428ee8..29b1a44bcb 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/IndexPatternTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/IndexPatternTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/MFlsTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/MFlsTest.java index 6267aeb9c0..f5408113b6 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/MFlsTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/MFlsTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.dlsfls; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/dlsfls/RenameFieldResponseProcessorTest.java b/src/test/java/org/opensearch/security/dlic/dlsfls/RenameFieldResponseProcessorTest.java index c22d167b6d..55aff8f470 100644 --- a/src/test/java/org/opensearch/security/dlic/dlsfls/RenameFieldResponseProcessorTest.java +++ b/src/test/java/org/opensearch/security/dlic/dlsfls/RenameFieldResponseProcessorTest.java @@ -12,7 +12,7 @@ package org.opensearch.security.dlic.dlsfls; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.action.index.IndexRequest; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java index f2df09549f..4b2e9e4417 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java @@ -116,10 +116,12 @@ protected CType getConfigType() { } - protected JsonNode xContentToJsonNode(final ToXContent toXContent) throws IOException { + protected JsonNode xContentToJsonNode(final ToXContent toXContent) { try (final var xContentBuilder = XContentFactory.jsonBuilder()) { toXContent.toXContent(xContentBuilder, ToXContent.EMPTY_PARAMS); return DefaultObjectMapper.readTree(xContentBuilder.toString()); + } catch (final IOException ioe) { + throw new RuntimeException(ioe); } } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java index f9c4428bc1..c3c2106b05 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractRestApiUnitTest.java @@ -19,7 +19,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.opensearch.common.settings.Settings; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java deleted file mode 100644 index f84e28e755..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AccountApiTest.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.security.securityconf.impl.CType; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; - -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; - -public class AccountApiTest extends AbstractRestApiUnitTest { - private final String BASE_ENDPOINT; - private final String ENDPOINT; - - protected String getEndpointPrefix() { - return PLUGINS_PREFIX; - } - - public AccountApiTest() { - BASE_ENDPOINT = getEndpointPrefix() + "/api/"; - ENDPOINT = getEndpointPrefix() + "/api/account"; - } - - @Test - public void testGetAccount() throws Exception { - // arrange - setup(); - final String testUser = "test-user"; - final String testPass = "some password for user"; - addUserWithPassword(testUser, testPass, HttpStatus.SC_CREATED); - - // test - unauthorized access as credentials are missing. - HttpResponse response = rh.executeGetRequest(ENDPOINT, new Header[0]); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - incorrect password - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader(testUser, "wrong-pass")); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - incorrect user - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("wrong-user", testPass)); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - valid request - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader(testUser, testPass)); - Settings body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - assertEquals(testUser, body.get("user_name")); - assertFalse(body.getAsBoolean("is_reserved", true)); - assertFalse(body.getAsBoolean("is_hidden", true)); - assertTrue(body.getAsBoolean("is_internal_user", false)); - assertNull(body.get("user_requested_tenant")); - assertNotNull(body.getAsList("backend_roles").size()); - assertNotNull(body.getAsList("custom_attribute_names").size()); - assertNotNull(body.getAsSettings("tenants")); - assertNotNull(body.getAsList("roles")); - } - - @Test - public void testPutAccount() throws Exception { - // arrange - setup(); - final String testUser = "test-user"; - final String testPass = "test-old-pass"; - final String testPassHash = "$2y$12$b7TNPn2hgl0nS7gXJ.beuOd8JGl6Nz5NsTyxofglGCItGNyDdwivK"; // hash for test-old-pass - final String testNewPass = "test-new-pass"; - final String testNewPassHash = "$2y$12$cclJJdVdXMMVzkhqQhEoE.hoERKE8bDzctR0S3aYj2EPHq45Y.GXC"; // hash for test-old-pass - addUserWithPassword(testUser, testPass, HttpStatus.SC_CREATED); - - // test - unauthorized access as credentials are missing. - HttpResponse response = rh.executePutRequest(ENDPOINT, "", new Header[0]); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - bad request as body is missing - response = rh.executePutRequest(ENDPOINT, "", encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as current password is missing - String payload = "{\"password\":\"new-pass\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as current password is incorrect - payload = "{\"password\":\"" + testNewPass + "\", \"current_password\":\"" + "wrong-pass" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as hash/password is missing - payload = "{\"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as password is empty - payload = "{\"password\":\"" + "" + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as hash is empty - payload = "{\"hash\":\"" + "" + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as hash and password are empty - payload = "{\"hash\": \"\", \"password\": \"\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - bad request as invalid parameters are present - payload = "{\"password\":\"new-pass\", \"current_password\":\"" + testPass + "\", \"backend_roles\": []}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatusCode()); - - // test - invalid user - payload = "{\"password\":\"" + testNewPass + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("wrong-user", testPass)); - assertEquals(HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - // test - valid password change with hash - payload = "{\"hash\":\"" + testNewPassHash + "\", \"current_password\":\"" + testPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testPass)); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // test - valid password change - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + testNewPass + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader(testUser, testNewPass)); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // create users from - resources/restapi/internal_users.yml - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(BASE_ENDPOINT + CType.INTERNALUSERS.toLCString()); - rh.sendAdminCertificate = false; - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - - // test - reserved user - sarek - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("sarek", "sarek")); - Settings body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - // check reserved user exists - assertTrue(body.getAsBoolean("is_reserved", false)); - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + "sarek" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("sarek", "sarek")); - assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // test - hidden user - hide - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("hide", "hide")); - body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - // check hidden user exists - assertTrue(body.getAsBoolean("is_hidden", false)); - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + "hide" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("hide", "hide")); - assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - - // test - admin with admin cert - internal user does not exist - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(ENDPOINT, encodeBasicHeader("admin", "admin")); - body = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - assertEquals("CN=kirk,OU=client,O=client,L=Test,C=DE", body.get("user_name")); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); // check admin user exists - payload = "{\"password\":\"" + testPass + "\", \"current_password\":\"" + "admin" + "\"}"; - response = rh.executePutRequest(ENDPOINT, payload, encodeBasicHeader("admin", "admin")); - assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatusCode()); - } - - @Test - public void testPutAccountRetainsAccountInformation() throws Exception { - // arrange - setup(); - final String testUsername = "test"; - final String testPassword = "test-password"; - final String newPassword = "new-password"; - final String createInternalUserPayload = "{\n" - + " \"password\": \"" - + testPassword - + "\",\n" - + " \"backend_roles\": [\"test-backend-role-1\"],\n" - + " \"opendistro_security_roles\": [\"opendistro_security_all_access\"],\n" - + " \"attributes\": {\n" - + " \"attribute1\": \"value1\"\n" - + " }\n" - + "}"; - final String changePasswordPayload = "{\"password\":\"" + newPassword + "\", \"current_password\":\"" + testPassword + "\"}"; - final String internalUserEndpoint = BASE_ENDPOINT + "internalusers/" + testUsername; - - // create user - rh.sendAdminCertificate = true; - HttpResponse response = rh.executePutRequest(internalUserEndpoint, createInternalUserPayload); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - rh.sendAdminCertificate = false; - - // change password to new-password - response = rh.executePutRequest(ENDPOINT, changePasswordPayload, encodeBasicHeader(testUsername, testPassword)); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - - // assert account information has not changed - rh.sendAdminCertificate = true; - response = rh.executeGetRequest(internalUserEndpoint); - assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - Settings responseBody = Settings.builder() - .loadFromSource(response.getBody(), XContentType.JSON) - .build() - .getAsSettings(testUsername); - assertTrue(responseBody.getAsList("backend_roles").contains("test-backend-role-1")); - assertTrue(responseBody.getAsList("opendistro_security_roles").contains("opendistro_security_all_access")); - assertEquals(responseBody.getAsSettings("attributes").get("attribute1"), "value1"); - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java index 46b730abac..fbeb3473fc 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiTest.java @@ -17,7 +17,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -441,25 +441,46 @@ public void testActionGroupsApiForActionGroupsRestApiAdmin() throws Exception { } @Test - public void testCreateActionGroupWithRestAdminPermissionsForbidden() throws Exception { + public void testCreateOrUpdateRestApiAdminActionGroupForbidden() throws Exception { setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); rh.sendAdminCertificate = false; - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header restApiAdminActionGroupsHeader = encodeBasicHeader("rest_api_admin_actiongroups", "rest_api_admin_actiongroups"); - final Header restApiHeader = encodeBasicHeader("test", "test"); + final var userHeaders = List.of( + encodeBasicHeader("admin", "admin"), + encodeBasicHeader("test", "test"), + encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"), + encodeBasicHeader("rest_api_admin_actiongroups", "rest_api_admin_actiongroups") + ); + for (final var userHeader : userHeaders) { + // attempt to create new action group with REST admin permissions + verifyPutForbidden("new_rest_api_admin_group", restAdminAllowedActions(), userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("new_rest_api_admin_group", "add"), userHeader); + + // attempt to update existing action group which has REST admin permissions + verifyPutForbidden("rest_admin_action_group", restAdminAllowedActions(), userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_admin_action_group", "replace"), userHeader); + + // attempt to update existing action group with REST admin permissions + verifyPutForbidden("OPENDISTRO_SECURITY_CLUSTER_ALL", restAdminAllowedActions(), userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("OPENDISTRO_SECURITY_CLUSTER_ALL", "replace"), userHeader); + + // attempt to delete + verifyDeleteForbidden("rest_admin_action_group", userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_admin_action_group", "remove"), userHeader); + } + } - HttpResponse response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiAdminActionGroupsHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePutRequest(ENDPOINT + "/rest_api_admin_group", restAdminAllowedActions(), restApiHeader); + void verifyPutForbidden(final String actionGroupName, final String payload, final Header... header) { + HttpResponse response = rh.executePutRequest(ENDPOINT + "/" + actionGroupName, payload, header); Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiAdminActionGroupsHeader); + void verifyPatchForbidden(final String payload, final Header... header) { + HttpResponse response = rh.executePatchRequest(ENDPOINT, payload, header); Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePatchRequest(ENDPOINT, restAdminPatchBody(), restApiHeader); + } + + void verifyDeleteForbidden(final String actionGroupName, final Header... header) { + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/" + actionGroupName, header); Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } @@ -469,13 +490,30 @@ String restAdminAllowedActions() throws JsonProcessingException { return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); } - String restAdminPatchBody() throws JsonProcessingException { + private String createPatchRestAdminPermissionsPayload(final String actionGroup, final String op) throws JsonProcessingException { final ArrayNode rootNode = DefaultObjectMapper.objectMapper.createArrayNode(); - final ObjectNode opAddRootNode = DefaultObjectMapper.objectMapper.createObjectNode(); + final ObjectNode opAddObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); final ObjectNode allowedActionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); allowedActionsNode.set("allowed_actions", clusterPermissionsForRestAdmin("cluster/*")); - opAddRootNode.put("op", "add").put("path", "/rest_api_admin_group").set("value", allowedActionsNode); - rootNode.add(opAddRootNode); + if ("add".equals(op)) { + opAddObjectNode.put("op", "add").put("path", "/" + actionGroup).set("value", allowedActionsNode); + rootNode.add(opAddObjectNode); + } + + if ("remove".equals(op)) { + final ObjectNode opRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + opRemoveObjectNode.put("op", "remove").put("path", "/" + actionGroup); + rootNode.add(opRemoveObjectNode); + } + + if ("replace".equals(op)) { + final ObjectNode replaceRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + replaceRemoveObjectNode.put("op", "replace") + .put("path", "/" + actionGroup + "/allowed_actions") + .set("value", clusterPermissionsForRestAdmin("*")); + + rootNode.add(replaceRemoveObjectNode); + } return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); } diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java index ccce614c07..567421e426 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AllowlistApiTest.java @@ -17,7 +17,7 @@ import com.google.common.collect.ImmutableMap; import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java index ce72fe2cef..92ce7c9112 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AuditApiActionTest.java @@ -25,7 +25,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.After; import org.junit.Rule; import org.junit.Test; @@ -682,7 +682,7 @@ private String getTestPayload() { + "\"enable_rest\":true,\"disabled_rest_categories\":[\"AUTHENTICATED\"]," + "\"enable_transport\":true,\"disabled_transport_categories\":[\"SSL_EXCEPTION\"]," + "\"resolve_bulk_requests\":true,\"log_request_body\":true,\"resolve_indices\":true,\"exclude_sensitive_headers\":true," - + "\"ignore_users\":[\"test-user-1\"],\"ignore_requests\":[\"test-request\"]}," + + "\"ignore_users\":[\"test-user-1\"],\"ignore_requests\":[\"test-request\"], \"ignore_headers\":[\"\"], \"ignore_url_params\":[]}," + "\"compliance\":{" + "\"enabled\":true," + "\"internal_config\":true,\"external_config\":true," diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiActionUnitTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiActionUnitTest.java new file mode 100644 index 0000000000..36407cfbc4 --- /dev/null +++ b/src/test/java/org/opensearch/security/dlic/rest/api/ConfigUpgradeApiActionUnitTest.java @@ -0,0 +1,291 @@ +/* + * 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 java.io.IOException; +import java.util.List; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableList; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.action.index.IndexResponse; +import org.opensearch.client.Client; +import org.opensearch.common.action.ActionFuture; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.RestResponse; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.support.Utils; +import org.opensearch.security.dlic.rest.validation.ValidationResult; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; + +import org.mockito.Mock; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +public class ConfigUpgradeApiActionUnitTest extends AbstractApiActionValidationTest { + + @Mock + private Client client; + + @Mock + private RestChannel restChannel; + + @Mock + private RestRequest restRequest; + + private ConfigUpgradeApiAction configUpgradeApiAction; + + @Before + public void setUp() throws IOException { + setupRolesConfiguration(); + doReturn(XContentFactory.jsonBuilder()).when(restChannel).newBuilder(); + + final var actionFuture = mock(ActionFuture.class); + doReturn(mock(IndexResponse.class)).when(actionFuture).actionGet(); + doReturn(actionFuture).when(client).index(any()); + + configUpgradeApiAction = spy(new ConfigUpgradeApiAction(clusterService, threadPool, securityApiDependencies)); + + final var objectMapper = DefaultObjectMapper.objectMapper; + final var config = objectMapper.createObjectNode(); + config.set("_meta", objectMapper.createObjectNode().put("type", CType.ROLES.toLCString()).put("config_version", 2)); + config.set("kibana_read_only", objectMapper.createObjectNode().put("reserved", true)); + final var newRole = objectMapper.createObjectNode(); + newRole.put("reserved", true); + newRole.putArray("cluster_permissions").add("test-permission-1").add("test-permission-2"); + config.set("new_role", newRole); + + doReturn(config).when(configUpgradeApiAction).loadConfigFileAsJson(any()); + } + + @Test + public void testCanUpgrade_ErrorLoadingConfig() throws Exception { + // Setup + doThrow(new IOException("abc")).when(configUpgradeApiAction).loadConfigFileAsJson(any()); + + // Execute + configUpgradeApiAction.canUpgrade(restChannel, restRequest, client); + + // Assert + verify(restChannel).sendResponse(verifyResponseBody(body -> assertThat(body, containsString("see the log file to troubleshoot")))); + } + + @Test + public void testPerformUpgrade_ErrorLoadingConfig() throws Exception { + // Setup + doThrow(new IOException("abc")).when(configUpgradeApiAction).loadConfigFileAsJson(any()); + + // Execute + configUpgradeApiAction.performUpgrade(restChannel, restRequest, client); + + // Assert + verify(restChannel).sendResponse(verifyResponseBody(body -> assertThat(body, containsString("see the log file to troubleshoot")))); + } + + @Test + public void testPerformUpgrade_ErrorApplyConfig() throws Exception { + // Setup + doThrow(new RuntimeException("abc")).when(configUpgradeApiAction).patchEntities(any(), any(), any()); + + // Execute + configUpgradeApiAction.performUpgrade(restChannel, restRequest, client); + + // Assert + verify(restChannel).sendResponse(verifyResponseBody(body -> assertThat(body, containsString("see the log file to troubleshoot")))); + } + + @Test + public void testPerformUpgrade_NoDifferences() throws Exception { + // Setup + final var rolesCopy = rolesConfiguration.deepClone(); + rolesCopy.removeStatic(); // Statics are added by code, not by config files, they should be omitted + final var rolesJsonNode = Utils.convertJsonToJackson(rolesCopy, true); + doReturn(rolesJsonNode).when(configUpgradeApiAction).loadConfigFileAsJson(any()); + + // Execute + configUpgradeApiAction.performUpgrade(restChannel, restRequest, client); + + // Verify + verify(restChannel).sendResponse(verifyResponseBody(body -> assertThat(body, containsString("no differences found")))); + } + + @Test + public void testPerformUpgrade_WithDifferences() throws Exception { + // Execute + configUpgradeApiAction.performUpgrade(restChannel, restRequest, client); + + // Verify + verify(restChannel).sendResponse(argThat(response -> { + final var rawResponseBody = response.content().utf8ToString(); + final var newlineNormalizedBody = rawResponseBody.replace("\r\n", "\n"); + assertThat(newlineNormalizedBody, equalTo("{\n" + // + " \"status\" : \"OK\",\n" + // + " \"upgrades\" : {\n" + // + " \"roles\" : {\n" + // + " \"add\" : [ \"new_role\" ]\n" + // + " }\n" + // + " }\n" + // + "}")); + return true; + })); + } + + @Test + public void testConfigurationDifferences_OperationBash() throws IOException { + final var testCases = new ImmutableList.Builder(); + + testCases.add( + new OperationTestCase("Missing entry", source -> {}, updated -> updated.put("a", "1"), List.of(List.of("add", "/a", "1"))) + ); + + testCases.add( + new OperationTestCase( + "Same object", + source -> source.set("a", objectMapper.createObjectNode()), + updated -> updated.set("a", objectMapper.createObjectNode()), + List.of() + ) + ); + + testCases.add( + new OperationTestCase("Missing object", source -> source.set("a", objectMapper.createObjectNode()), updated -> {}, List.of()) + ); + + testCases.add(new OperationTestCase("Moved and identical object", source -> { + source.set("a", objectMapper.createObjectNode()); + source.set("b", objectMapper.createObjectNode()); + source.set("c", objectMapper.createObjectNode()); + }, updated -> { + updated.set("a", objectMapper.createObjectNode()); + updated.set("c", objectMapper.createObjectNode()); + updated.set("b", objectMapper.createObjectNode()); + }, List.of())); + + testCases.add(new OperationTestCase("Moved and different object", source -> { + source.set("a", objectMapper.createObjectNode()); + source.set("b", objectMapper.createObjectNode()); + source.set("c", objectMapper.createObjectNode()); + }, updated -> { + updated.set("a", objectMapper.createObjectNode()); + updated.set("c", objectMapper.createObjectNode().put("d", "1")); + updated.set("b", objectMapper.createObjectNode()); + }, List.of(List.of("add", "/c/d", "1")))); + + testCases.add(new OperationTestCase("Removed field object", source -> { + source.set("a", objectMapper.createObjectNode().put("hidden", true)); + source.set("b", objectMapper.createObjectNode()); + }, updated -> { + updated.set("a", objectMapper.createObjectNode()); + updated.set("b", objectMapper.createObjectNode()); + }, List.of(List.of("remove", "/a/hidden", "")))); + + testCases.add(new OperationTestCase("Removed field object", source -> { + final var roleA = objectMapper.createObjectNode(); + roleA.putArray("cluster_permissions").add("1").add("2").add("3"); + source.set("a", roleA); + }, updated -> { + final var roleA = objectMapper.createObjectNode(); + roleA.putArray("cluster_permissions").add("2").add("11").add("3").add("44"); + updated.set("a", roleA); + }, + List.of( + List.of("remove", "/a/cluster_permissions/0", ""), + List.of("add", "/a/cluster_permissions/1", "11"), + List.of("add", "/a/cluster_permissions/3", "44") + ) + )); + + for (final var tc : testCases.build()) { + // Setup + final var source = objectMapper.createObjectNode(); + source.set("_meta", objectMapper.createObjectNode().put("type", CType.ROLES.toLCString()).put("config_version", 2)); + tc.sourceChanges.accept(source); + final var updated = objectMapper.createObjectNode(); + tc.updates.accept(updated); + + var sourceAsConfig = SecurityDynamicConfiguration.fromJson(objectMapper.writeValueAsString(source), CType.ROLES, 2, 1, 1); + + doReturn(ValidationResult.success(sourceAsConfig)).when(configUpgradeApiAction) + .loadConfiguration(any(), anyBoolean(), anyBoolean()); + doReturn(updated).when(configUpgradeApiAction).loadConfigFileAsJson(any()); + + // Execute + var result = configUpgradeApiAction.computeDifferenceToUpdate(CType.ACTIONGROUPS); + + // Verify + result.valid(differences -> { + assertThat(differences.v1(), equalTo(CType.ACTIONGROUPS)); + assertThat(tc.name + ": Number of operations", differences.v2().size(), equalTo(tc.expectedResults.size())); + final var expectedResultsIterator = tc.expectedResults.iterator(); + differences.v2().forEach(operation -> { + final List expected = expectedResultsIterator.next(); + assertThat( + tc.name + ": Operation type" + operation.toPrettyString(), + operation.get("op").asText(), + equalTo(expected.get(0)) + ); + assertThat(tc.name + ": Path" + operation.toPrettyString(), operation.get("path").asText(), equalTo(expected.get(1))); + assertThat( + tc.name + ": Value " + operation.toPrettyString(), + operation.has("value") ? operation.get("value").asText("") : "", + equalTo(expected.get(2)) + ); + }); + }); + } + } + + static class OperationTestCase { + final String name; + final Consumer sourceChanges; + final Consumer updates; + final List> expectedResults; + + OperationTestCase( + final String name, + final Consumer sourceChanges, + final Consumer updates, + final List> expectedResults + ) { + this.name = name; + this.sourceChanges = sourceChanges; + this.updates = updates; + this.expectedResults = expectedResults; + } + + } + + private RestResponse verifyResponseBody(final Consumer test) { + return argThat(response -> { + final String content = response.content().utf8ToString(); + test.accept(content); + return true; + }); + } + +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/DashboardsInfoActionTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/DashboardsInfoActionTest.java deleted file mode 100644 index 46128f5a71..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/DashboardsInfoActionTest.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import org.apache.hc.core5.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.helper.rest.RestHelper; - -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; - -public class DashboardsInfoActionTest extends AbstractRestApiUnitTest { - private final String ENDPOINT; - - protected String getEndpoint() { - return PLUGINS_PREFIX + "/dashboardsinfo"; - } - - public DashboardsInfoActionTest() { - ENDPOINT = getEndpoint(); - } - - @Test - public void testDashboardsInfo() throws Exception { - Settings settings = Settings.builder() - .put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true) - .build(); - setup(settings); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - RestHelper.HttpResponse response = rh.executeGetRequest(ENDPOINT); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - } - -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/FlushCacheApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/FlushCacheApiTest.java deleted file mode 100644 index 120596f046..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/FlushCacheApiTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; - -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; - -public class FlushCacheApiTest extends AbstractRestApiUnitTest { - private final String ENDPOINT; - - protected String getEndpointPrefix() { - return PLUGINS_PREFIX; - } - - public FlushCacheApiTest() { - ENDPOINT = getEndpointPrefix() + "/api/cache"; - } - - @Test - public void testFlushCache() throws Exception { - - setup(); - - // Only DELETE is allowed for flush cache - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - // GET - HttpResponse response = rh.executeGetRequest(ENDPOINT); - Assert.assertEquals(HttpStatus.SC_NOT_IMPLEMENTED, response.getStatusCode()); - Settings settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Method GET not supported for this action."); - - // PUT - response = rh.executePutRequest(ENDPOINT, "{}", new Header[0]); - Assert.assertEquals(HttpStatus.SC_NOT_IMPLEMENTED, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Method PUT not supported for this action."); - - // POST - response = rh.executePostRequest(ENDPOINT, "{}", new Header[0]); - Assert.assertEquals(HttpStatus.SC_NOT_IMPLEMENTED, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Method POST not supported for this action."); - - // DELETE - response = rh.executeDeleteRequest(ENDPOINT, new Header[0]); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - settings = Settings.builder().loadFromSource(response.getBody(), XContentType.JSON).build(); - Assert.assertEquals(settings.get("message"), "Cache flushed successfully."); - - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/GetConfigurationApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/GetConfigurationApiTest.java index 09c4a762b5..8defebc6d1 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/GetConfigurationApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/GetConfigurationApiTest.java @@ -12,7 +12,7 @@ package org.opensearch.security.dlic.rest.api; import com.fasterxml.jackson.databind.JsonNode; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/IndexMissingTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/IndexMissingTest.java index aefb0f2550..4632a3920f 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/IndexMissingTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/IndexMissingTest.java @@ -12,7 +12,7 @@ package org.opensearch.security.dlic.rest.api; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java index 773d356246..2af598f5d5 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/InternalUsersApiActionValidationTest.java @@ -22,6 +22,7 @@ import org.opensearch.core.rest.RestStatus; import org.opensearch.rest.RestRequest; import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.dlic.rest.validation.ValidationResult; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; import org.opensearch.security.securityconf.impl.v7.InternalUserV7; @@ -32,9 +33,13 @@ import org.mockito.Mock; import org.mockito.Mockito; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.containsString; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.Mockito.when; public class InternalUsersApiActionValidationTest extends AbstractApiActionValidationTest { @@ -145,19 +150,26 @@ public void validateSecurityRolesWithMutableRolesMappingConfig() throws Exceptio // should ok to set regular role with mutable role mapping var userJson = objectMapper.createObjectNode().set("opendistro_security_roles", objectMapper.createArrayNode().add("regular_role")); var result = internalUsersApiAction.validateSecurityRoles(SecurityConfiguration.of(userJson, "some_user", configuration)); - assertTrue(result.isValid()); + assertValidationResultIsValid(result); // should be ok to set reserved role with mutable role mapping userJson = objectMapper.createObjectNode().set("opendistro_security_roles", objectMapper.createArrayNode().add("kibana_read_only")); result = internalUsersApiAction.validateSecurityRoles(SecurityConfiguration.of(userJson, "some_user", configuration)); - assertTrue(result.isValid()); + assertValidationResultIsValid(result); // should be ok to set static role with mutable role mapping userJson = objectMapper.createObjectNode().set("opendistro_security_roles", objectMapper.createArrayNode().add("all_access")); result = internalUsersApiAction.validateSecurityRoles(SecurityConfiguration.of(userJson, "some_user", configuration)); - assertTrue(result.isValid()); + assertValidationResultIsValid(result); // should not be ok to set hidden role with mutable role mapping userJson = objectMapper.createObjectNode().set("opendistro_security_roles", objectMapper.createArrayNode().add("some_hidden_role")); result = internalUsersApiAction.validateSecurityRoles(SecurityConfiguration.of(userJson, "some_user", configuration)); - assertFalse(result.isValid()); + final var errorMessage = xContentToJsonNode(result.errorMessage()).toPrettyString(); + assertThat(errorMessage, allOf(containsString("NOT_FOUND"), containsString("Resource 'some_hidden_role' is not available."))); + } + + void assertValidationResultIsValid(final ValidationResult result) { + if (!result.isValid()) { + fail("Expected valid result, error message: " + xContentToJsonNode(result.errorMessage()).toPrettyString()); + } } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java index 7132dcc491..752335b802 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiTest.java @@ -12,13 +12,16 @@ package org.opensearch.security.dlic.rest.api; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Test; +import org.opensearch.security.securityconf.impl.DashboardSignInOption; import org.opensearch.security.support.ConfigConstants; import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.not; import static org.hamcrest.core.IsEqual.equalTo; import static org.hamcrest.core.StringContains.containsString; @@ -54,9 +57,43 @@ private void verifyTenantUpdate(final Header... header) throws Exception { setPrivateTenantAsDefaultResponse.getStatusCode(), equalTo(HttpStatus.SC_OK) ); + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), hasItem(DashboardSignInOption.BASIC.toString())); + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), not(hasItem(DashboardSignInOption.SAML.toString()))); + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), not(hasItem(DashboardSignInOption.OPENID.toString()))); + + final HttpResponse updateDashboardSignInOptions = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"sign_in_options\": [\"BASIC\", \"OPENID\"]}", + header + ); + assertThat(updateDashboardSignInOptions.getBody(), updateDashboardSignInOptions.getStatusCode(), equalTo(HttpStatus.SC_OK)); + getDashboardsinfoResponse = rh.executeGetRequest("/_plugins/_security/dashboardsinfo", ADMIN_FULL_ACCESS_USER); assertThat(getDashboardsinfoResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); assertThat(getDashboardsinfoResponse.findValueInJson("default_tenant"), equalTo("Private")); + + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), hasItem((DashboardSignInOption.BASIC.toString()))); + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), hasItem((DashboardSignInOption.OPENID.toString()))); + + final HttpResponse updateUnavailableSignInOption = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"sign_in_options\": [\"BASIC\", \"SAML\"]}", + header + ); + assertThat(updateUnavailableSignInOption.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + assertThat( + updateUnavailableSignInOption.findValueInJson("error.reason"), + containsString("Validation failure: SAML authentication provider is not available for this cluster.") + ); + + // Ensuring the sign in options array has not been modified due to the Bad Request response. + getDashboardsinfoResponse = rh.executeGetRequest("/_plugins/_security/dashboardsinfo", ADMIN_FULL_ACCESS_USER); + assertThat(getDashboardsinfoResponse.getStatusCode(), equalTo(HttpStatus.SC_OK)); + + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options").size(), equalTo(2)); + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), hasItem(DashboardSignInOption.BASIC.toString())); + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), hasItem(DashboardSignInOption.OPENID.toString())); + assertThat(getDashboardsinfoResponse.findArrayInJson("sign_in_options"), not(hasItem(DashboardSignInOption.SAML.toString()))); } @Test @@ -148,6 +185,30 @@ private void verifyTenantUpdateFailed(final Header... header) throws Exception { setRandomStringAsDefaultTenant.findValueInJson("error.reason"), containsString("Default tenant should be selected from one of the available tenants.") ); + + final HttpResponse signInOptionsNonArrayValue = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"sign_in_options\": \"BASIC\"}", + header + ); + assertThat(signInOptionsNonArrayValue.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + assertThat( + signInOptionsNonArrayValue.getBody(), + signInOptionsNonArrayValue.findValueInJson("reason"), + containsString("Wrong datatype") + ); + + final HttpResponse invalidSignInOption = rh.executePutRequest( + "/_plugins/_security/api/tenancy/config", + "{\"sign_in_options\": [\"INVALID_OPTION\"]}", + header + ); + assertThat(invalidSignInOption.getStatusCode(), equalTo(HttpStatus.SC_BAD_REQUEST)); + assertThat( + invalidSignInOption.getBody(), + invalidSignInOption.findValueInJson("error.reason"), + containsString("authentication provider is not available for this cluster") + ); } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java index 44c43863f9..8379a80989 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/NodesDnApiTest.java @@ -20,7 +20,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java index cbd751e00c..1f4d0ff247 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RoleBasedAccessTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.rest.api; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java index 88a358dcb2..7fe089c0ba 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiActionValidationTest.java @@ -28,21 +28,18 @@ public class RolesApiActionValidationTest extends AbstractApiActionValidationTes @Test public void isAllowedToChangeImmutableEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLES)).thenReturn(true); - final var role = new RoleV7(); role.setCluster_permissions(restApiAdminPermissions()); - final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies).createEndpointValidator(); - final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of( "sss", configuration)); + final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("sss", configuration)); assertTrue(result.isValid()); } @Test public void isNotAllowedRightsToChangeImmutableEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLES)).thenReturn(false); - final var role = new RoleV7(); role.setCluster_permissions(restApiAdminPermissions()); @@ -50,8 +47,9 @@ public void isNotAllowedRightsToChangeImmutableEntity() throws Exception { Mockito.when(configuration.getCEntry("sss")).thenReturn(role); when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); - final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies).createEndpointValidator(); - final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of( "sss", configuration)); + final var rolesApiActionEndpointValidator = new RolesApiAction(clusterService, threadPool, securityApiDependencies) + .createEndpointValidator(); + final var result = rolesApiActionEndpointValidator.isAllowedToChangeImmutableEntity(SecurityConfiguration.of("sss", configuration)); assertFalse(result.isValid()); assertEquals(RestStatus.FORBIDDEN, result.status()); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java index 8b475ec776..eb3ad7d4e5 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesApiTest.java @@ -18,7 +18,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -691,105 +691,74 @@ public void testRolesApiWithRestApiRolePermission() throws Exception { } @Test - public void testCreateOrUpdateRestApiAdminRoleForbiddenForNonSuperAdmin() throws Exception { + public void testCrudRestApiAdminRoleForbidden() throws Exception { setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); rh.sendAdminCertificate = false; - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header adminHeader = encodeBasicHeader("admin", "admin"); - final Header restApiHeader = encodeBasicHeader("test", "test"); - - final String restAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_admin_role", - restAdminPermissionsPayload, - restApiAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - response = rh.executePutRequest(ENDPOINT + "/roles/rest_admin_role_to_delete", restAdminPermissionsPayload, restApiAdminHeader); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - - // attempt to create a new rest admin role by admin - response = rh.executePutRequest(ENDPOINT + "/roles/some_rest_admin_role", restAdminPermissionsPayload, adminHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to update exiting admin role - response = rh.executePutRequest(ENDPOINT + "/roles/new_rest_admin_role", restAdminPermissionsPayload, adminHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to patch exiting admin role - response = rh.executePatchRequest( - ENDPOINT + "/roles/new_rest_admin_role", - createPatchRestAdminPermissionsPayload("replace"), - adminHeader - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to update exiting admin role - response = rh.executePutRequest(ENDPOINT + "/roles/new_rest_admin_role", restAdminPermissionsPayload, restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to create a new rest admin role by admin - response = rh.executePutRequest(ENDPOINT + "/roles/some_rest_admin_role", restAdminPermissionsPayload, restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - // attempt to patch exiting admin role and crate a new one - response = rh.executePatchRequest(ENDPOINT + "/roles", createPatchRestAdminPermissionsPayload("replace"), restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + final var userHeaders = List.of( + encodeBasicHeader("admin", "admin"), + encodeBasicHeader("test", "test"), + encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"), + encodeBasicHeader("rest_api_admin_roles", "rest_api_admin_roles") + ); + for (final var userHeader : userHeaders) { + final String restAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); + // attempt to create a new role + verifyPutForbidden("new_rest_admin_role", restAdminPermissionsPayload, userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("new_rest_admin_role", "add"), userHeader); + + // attempt to update existing rest admin role + verifyPutForbidden("rest_api_admin_full_access", restAdminPermissionsPayload, userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_api_admin_full_access", "replace"), userHeader); + + // attempt to update non rest admin role with REST admin permissions + verifyPutForbidden("opendistro_security_role_starfleet_captains", restAdminPermissionsPayload, userHeader); + verifyPatchForbidden( + createPatchRestAdminPermissionsPayload("opendistro_security_role_starfleet_captains", "replace"), + userHeader + ); + + // attempt to remove REST admin role + verifyDeleteForbidden("rest_api_admin_full_access", userHeader); + verifyPatchForbidden(createPatchRestAdminPermissionsPayload("rest_api_admin_full_access", "remove"), userHeader); + } + } - response = rh.executePatchRequest(ENDPOINT + "/roles", createPatchRestAdminPermissionsPayload("add"), restApiHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePatchRequest(ENDPOINT + "/roles", createPatchRestAdminPermissionsPayload("remove"), restApiHeader); + void verifyPutForbidden(final String roleName, final String restAdminPermissionsPayload, final Header... header) { + HttpResponse response = rh.executePutRequest(ENDPOINT + "/roles/" + roleName, restAdminPermissionsPayload, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } - @Test - public void testDeleteRestApiAdminRoleForbiddenForNonSuperAdmin() throws Exception { - setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); - rh.sendAdminCertificate = false; - - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header adminHeader = encodeBasicHeader("admin", "admin"); - final Header restApiHeader = encodeBasicHeader("test", "test"); - - final String allRestAdminPermissionsPayload = createRestAdminPermissionsPayload("cluster/*"); - - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_admin_role", - allRestAdminPermissionsPayload, - restApiAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - - // attempt to update exiting admin role - response = rh.executeDeleteRequest(ENDPOINT + "/roles/new_rest_admin_role", adminHeader); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + void verifyPatchForbidden(final String restAdminPermissionsPayload, final Header... header) { + HttpResponse response = rh.executePatchRequest(ENDPOINT + "/roles", restAdminPermissionsPayload, header); + Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - // true to change - response = rh.executeDeleteRequest(ENDPOINT + "/roles/new_rest_admin_role", allRestAdminPermissionsPayload, restApiHeader); + void verifyDeleteForbidden(final String roleName, final Header... header) { + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/roles/" + roleName, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } - private String createPatchRestAdminPermissionsPayload(final String op) throws JsonProcessingException { + private String createPatchRestAdminPermissionsPayload(final String roleName, final String op) throws JsonProcessingException { final ArrayNode rootNode = DefaultObjectMapper.objectMapper.createArrayNode(); final ObjectNode opAddObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); final ObjectNode clusterPermissionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); clusterPermissionsNode.set("cluster_permissions", clusterPermissionsForRestAdmin("cluster/*")); if ("add".equals(op)) { - opAddObjectNode.put("op", "add").put("path", "/some_rest_admin_role").set("value", clusterPermissionsNode); + opAddObjectNode.put("op", "add").put("path", "/" + roleName).set("value", clusterPermissionsNode); rootNode.add(opAddObjectNode); } if ("remove".equals(op)) { final ObjectNode opRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); - opRemoveObjectNode.put("op", "remove").put("path", "/rest_admin_role_to_delete"); + opRemoveObjectNode.put("op", "remove").put("path", "/" + roleName); rootNode.add(opRemoveObjectNode); } if ("replace".equals(op)) { final ObjectNode replaceRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); replaceRemoveObjectNode.put("op", "replace") - .put("path", "/new_rest_admin_role/cluster_permissions") + .put("path", "/" + roleName + "/cluster_permissions") .set("value", clusterPermissionsForRestAdmin("*")); rootNode.add(replaceRemoveObjectNode); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java index 5c041989a6..f7d1d4da0b 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiActionValidationTest.java @@ -35,18 +35,16 @@ public void setupRoles() throws Exception { @Test public void isAllowedRightsToChangeRoleEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(true); final var rolesMappingApiActionEndpointValidator = new RolesMappingApiAction(clusterService, threadPool, securityApiDependencies) .createEndpointValidator(); final var result = rolesMappingApiActionEndpointValidator.isAllowedToChangeImmutableEntity( - SecurityConfiguration.of("rest_api_admin_role", configuration) + SecurityConfiguration.of("rest_api_admin_role", configuration) ); assertTrue(result.isValid()); } @Test public void isNotAllowedNoRightsToChangeRoleEntity() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(false); when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); final var rolesApiActionEndpointValidator = @@ -61,7 +59,6 @@ public void isNotAllowedNoRightsToChangeRoleEntity() throws Exception { @Test public void onConfigChangeShouldCheckRoles() throws Exception { - when(restApiAdminPrivilegesEvaluator.isCurrentUserAdminFor(Endpoint.ROLESMAPPING)).thenReturn(false); when(restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(any(Object.class))).thenCallRealMethod(); when(configurationRepository.getConfigurationsFromIndex(List.of(CType.ROLES), false)) .thenReturn(Map.of(CType.ROLES, rolesConfiguration)); diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java index 8d9b76274c..dc2ba33e6e 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/RolesMappingApiTest.java @@ -14,10 +14,11 @@ import java.util.List; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ArrayNode; import com.fasterxml.jackson.databind.node.ObjectNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; @@ -558,97 +559,83 @@ void verifyNonSuperAdminUser(final Header[] header) throws Exception { } @Test - public void testChangeRestApiAdminRoleMappingForbiddenForNonSuperAdmin() throws Exception { + public void testChangeRestApiAdminRoleMappingForbidden() throws Exception { setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); rh.sendAdminCertificate = false; - final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - final Header adminHeader = encodeBasicHeader("admin", "admin"); - final Header restApiHeader = encodeBasicHeader("test", "test"); - - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_api_role", - createRestAdminPermissionsPayload(), - restApiAdminHeader - ); - Assert.assertEquals(response.getBody(), HttpStatus.SC_CREATED, response.getStatusCode()); - response = rh.executePutRequest( - ENDPOINT + "/roles/new_rest_api_role_without_mapping", - createRestAdminPermissionsPayload(), - restApiAdminHeader + final var userHeaders = List.of( + encodeBasicHeader("admin", "admin"), + encodeBasicHeader("test", "test"), + encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"), + encodeBasicHeader("rest_api_admin_rolesmapping", "rest_api_admin_rolesmapping") ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - response = rh.executePutRequest( - ENDPOINT + "/rolesmapping/new_rest_api_role", - createUsersPayload("a", "b", "c"), - restApiAdminHeader - ); - Assert.assertEquals(HttpStatus.SC_CREATED, response.getStatusCode()); - verifyRestApiPutAndDeleteForNonRestApiAdmin(adminHeader); - verifyRestApiPutAndDeleteForNonRestApiAdmin(restApiHeader); - verifyRestApiPatchForNonRestApiAdmin(adminHeader, false); - verifyRestApiPatchForNonRestApiAdmin(restApiHeader, false); - verifyRestApiPatchForNonRestApiAdmin(adminHeader, true); - verifyRestApiPatchForNonRestApiAdmin(restApiHeader, true); - } + for (final var userHeader : userHeaders) { + // create new mapping for existing group + verifyPutForbidden("rest_api_admin_roles_mapping_test_without_mapping", createUsers("c", "d"), userHeader); + verifyPatchForbidden(createPatchPayload("rest_api_admin_roles_mapping_test_without_mapping", "add"), userHeader); - private void verifyRestApiPutAndDeleteForNonRestApiAdmin(final Header header) throws Exception { - HttpResponse response = rh.executePutRequest( - ENDPOINT + "/rolesmapping/new_rest_api_role", - createUsersPayload("a", "b", "c"), - header - ); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + // update existing mapping with additional users + verifyPutForbidden("rest_api_admin_roles_mapping_test_with_mapping", createUsers("c", "d"), userHeader); + verifyPatchForbidden(createPatchPayload("rest_api_admin_roles_mapping_test_with_mapping", "replace"), userHeader); - response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/new_rest_api_role", "", header); - Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + // delete existing role mapping forbidden + verifyDeleteForbidden("rest_api_admin_roles_mapping_test_with_mapping", userHeader); + verifyPatchForbidden(createPatchPayload("rest_api_admin_roles_mapping_test_with_mapping", "remove"), userHeader); + } } - private void verifyRestApiPatchForNonRestApiAdmin(final Header header, boolean bulk) throws Exception { - String path = ENDPOINT + "/rolesmapping"; - if (!bulk) { - path += "/new_rest_api_role"; - } - HttpResponse response = rh.executePatchRequest(path, createPathPayload("add"), header); - System.err.println(response.getBody()); + void verifyPutForbidden(final String roleMappingName, final String payload, final Header... header) { + HttpResponse response = rh.executePutRequest(ENDPOINT + "/rolesmapping/" + roleMappingName, payload, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - response = rh.executePatchRequest(path, createPathPayload("replace"), header); + void verifyPatchForbidden(final String payload, final Header... header) { + HttpResponse response = rh.executePatchRequest(ENDPOINT + "/rolesmapping", payload, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); + } - response = rh.executePatchRequest(path, createPathPayload("remove"), header); + void verifyDeleteForbidden(final String roleMappingName, final Header... header) { + HttpResponse response = rh.executeDeleteRequest(ENDPOINT + "/rolesmapping/" + roleMappingName, header); Assert.assertEquals(HttpStatus.SC_FORBIDDEN, response.getStatusCode()); } - private ObjectNode createUsersObjectNode(final String... users) { - final ArrayNode usersArray = DefaultObjectMapper.objectMapper.createArrayNode(); - for (final String user : users) { - usersArray.add(user); + private String createPatchPayload(final String roleName, final String op) throws JsonProcessingException { + final ArrayNode rootNode = DefaultObjectMapper.objectMapper.createArrayNode(); + final ObjectNode opAddObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + final ObjectNode clusterPermissionsNode = DefaultObjectMapper.objectMapper.createObjectNode(); + clusterPermissionsNode.set("users", createUsersArray("c", "d")); + if ("add".equals(op)) { + opAddObjectNode.put("op", "add").put("path", "/" + roleName).set("value", clusterPermissionsNode); + rootNode.add(opAddObjectNode); } - return DefaultObjectMapper.objectMapper.createObjectNode().set("users", usersArray); - } - private String createUsersPayload(final String... users) throws JsonProcessingException { - return DefaultObjectMapper.objectMapper.writeValueAsString(createUsersObjectNode(users)); - } - - private String createPathPayload(final String op) throws JsonProcessingException { - final ArrayNode arrayNode = DefaultObjectMapper.objectMapper.createArrayNode(); - final ObjectNode opNode = DefaultObjectMapper.objectMapper.createObjectNode(); - opNode.put("op", op); - if ("add".equals(op)) { - opNode.put("path", "/new_rest_api_role_without_mapping"); - opNode.set("value", createUsersObjectNode("d", "e", "f")); + if ("remove".equals(op)) { + final ObjectNode opRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + opRemoveObjectNode.put("op", "remove").put("path", "/" + roleName); + rootNode.add(opRemoveObjectNode); } + if ("replace".equals(op)) { - opNode.put("path", "/new_rest_api_role"); - opNode.set("value", createUsersObjectNode("g", "h", "i")); + final ObjectNode replaceRemoveObjectNode = DefaultObjectMapper.objectMapper.createObjectNode(); + replaceRemoveObjectNode.put("op", "replace").put("path", "/" + roleName + "/users").set("value", createUsersArray("c", "d")); + + rootNode.add(replaceRemoveObjectNode); } - if ("remove".equals(op)) { - opNode.put("path", "/new_rest_api_role"); + return DefaultObjectMapper.objectMapper.writeValueAsString(rootNode); + } + + private String createUsers(final String... users) throws JsonProcessingException { + final var o = DefaultObjectMapper.objectMapper.createObjectNode().set("users", createUsersArray("c", "d")); + return DefaultObjectMapper.writeValueAsString(o, false); + } + + private JsonNode createUsersArray(final String... users) { + final ArrayNode usersArray = DefaultObjectMapper.objectMapper.createArrayNode(); + for (final String user : users) { + usersArray.add(user); } - return DefaultObjectMapper.objectMapper.writeValueAsString(arrayNode.add(opNode)); + return usersArray; } @Test diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityApiAccessTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityApiAccessTest.java index 81fad7d4ff..1580d07524 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityApiAccessTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityApiAccessTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.rest.api; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; 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 7b98494e1b..c4066d11a2 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 @@ -12,7 +12,7 @@ package org.opensearch.security.dlic.rest.api; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityHealthActionTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityHealthActionTest.java deleted file mode 100644 index d7a6edfea9..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityHealthActionTest.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import org.apache.hc.core5.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.helper.rest.RestHelper; - -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; - -public class SecurityHealthActionTest extends AbstractRestApiUnitTest { - private final String ENDPOINT; - - protected String getEndpointPrefix() { - return PLUGINS_PREFIX; - } - - public SecurityHealthActionTest() { - ENDPOINT = getEndpointPrefix(); - } - - @Test - public void testSecurityHealthAPI() throws Exception { - Settings settings = Settings.builder() - .put(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true) - .build(); - setup(settings); - - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - - RestHelper.HttpResponse response = rh.executeGetRequest(ENDPOINT + "/health"); - Assert.assertEquals(HttpStatus.SC_OK, response.getStatusCode()); - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityInfoActionTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityInfoActionTest.java index db27be85ee..0799525eb8 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityInfoActionTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityInfoActionTest.java @@ -11,7 +11,7 @@ package org.opensearch.security.dlic.rest.api; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java deleted file mode 100644 index 75e1e59b0a..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SslCertsApiTest.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api; - -import java.util.List; -import java.util.Map; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; -import com.fasterxml.jackson.core.JsonProcessingException; -import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; -import org.junit.Assert; -import org.junit.Test; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.test.helper.rest.RestHelper.HttpResponse; - -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; -import static org.opensearch.security.support.ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED; - -public class SslCertsApiTest extends AbstractRestApiUnitTest { - - static final String HTTP_CERTS = "http"; - - static final String TRANSPORT_CERTS = "transport"; - - private final static List> EXPECTED_CERTIFICATES = ImmutableList.of( - ImmutableMap.of( - "issuer_dn", - "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", - "subject_dn", - "CN=node-0.example.com,OU=SSL,O=Test,L=Test,C=DE", - "san", - "[[2, node-0.example.com], [2, localhost], [7, 127.0.0.1], [8, 1.2.3.4.5.5]]", - "not_before", - "2018-05-05T14:37:09Z", - "not_after", - "2028-05-02T14:37:09Z" - ), - ImmutableMap.of( - "issuer_dn", - "CN=Example Com Inc. Root CA,OU=Example Com Inc. Root CA,O=Example Com Inc.,DC=example,DC=com", - "subject_dn", - "CN=Example Com Inc. Signing CA,OU=Example Com Inc. Signing CA,O=Example Com Inc.,DC=example,DC=com", - "san", - "", - "not_before", - "2018-05-05T14:37:08Z", - "not_after", - "2028-05-04T14:37:08Z" - ) - ); - - private final static String EXPECTED_CERTIFICATES_BY_TYPE; - static { - try { - EXPECTED_CERTIFICATES_BY_TYPE = DefaultObjectMapper.objectMapper.writeValueAsString( - ImmutableMap.of("http_certificates_list", EXPECTED_CERTIFICATES, "transport_certificates_list", EXPECTED_CERTIFICATES) - ); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - } - - private final Header restApiAdminHeader = encodeBasicHeader("rest_api_admin_user", "rest_api_admin_user"); - private final Header restApiCertsInfoAdminHeader = encodeBasicHeader("rest_api_admin_ssl_info", "rest_api_admin_ssl_info"); - - private final Header restApiReloadCertsAdminHeader = encodeBasicHeader( - "rest_api_admin_ssl_reloadcerts", - "rest_api_admin_ssl_reloadcerts" - ); - - private final Header restApiHeader = encodeBasicHeader("test", "test"); - - public String certsInfoEndpoint() { - return PLUGINS_PREFIX + "/api/ssl/certs"; - } - - public String certsReloadEndpoint(final String certType) { - return String.format("%s/api/ssl/%s/reloadcerts", PLUGINS_PREFIX, certType); - } - - private void verifyHasNoAccess() throws Exception { - final Header adminCredsHeader = encodeBasicHeader("admin", "admin"); - // No creds, no admin certificate - UNAUTHORIZED - rh.sendAdminCertificate = false; - HttpResponse response = rh.executeGetRequest(certsInfoEndpoint()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_UNAUTHORIZED, response.getStatusCode()); - - rh.sendAdminCertificate = false; - response = rh.executeGetRequest(certsInfoEndpoint(), adminCredsHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - response = rh.executeGetRequest(certsInfoEndpoint(), restApiHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - } - - @Test - public void testCertsInfo() throws Exception { - setup(); - verifyHasNoAccess(); - sendAdminCert(); - HttpResponse response = rh.executeGetRequest(certsInfoEndpoint()); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, response.getBody()); - - } - - @Test - public void testCertsInfoRestAdmin() throws Exception { - setupWithRestRoles(Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build()); - verifyHasNoAccess(); - rh.sendAdminCertificate = false; - Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, loadCerts(restApiAdminHeader)); - Assert.assertEquals(EXPECTED_CERTIFICATES_BY_TYPE, loadCerts(restApiCertsInfoAdminHeader)); - } - - private String loadCerts(final Header... header) throws Exception { - HttpResponse response = rh.executeGetRequest(certsInfoEndpoint(), restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_OK, response.getStatusCode()); - return response.getBody(); - } - - @Test - public void testReloadCertsNotAvailableByDefault() throws Exception { - setupWithRestRoles(); - - sendAdminCert(); - verifyReloadCertsNotAvailable(HttpStatus.SC_BAD_REQUEST); - - rh.sendAdminCertificate = false; - verifyReloadCertsNotAvailable(HttpStatus.SC_FORBIDDEN, restApiAdminHeader); - verifyReloadCertsNotAvailable(HttpStatus.SC_FORBIDDEN, restApiReloadCertsAdminHeader); - } - - private void verifyReloadCertsNotAvailable(final int expectedStatus, final Header... header) { - HttpResponse response = rh.executePutRequest(certsReloadEndpoint(HTTP_CERTS), "{}", header); - Assert.assertEquals(response.getBody(), expectedStatus, response.getStatusCode()); - response = rh.executePutRequest(certsReloadEndpoint(TRANSPORT_CERTS), "{}", header); - Assert.assertEquals(response.getBody(), expectedStatus, response.getStatusCode()); - } - - @Test - public void testReloadCertsWrongCertsType() throws Exception { - setupWithRestRoles(reloadEnabled()); - sendAdminCert(); - HttpResponse response = rh.executePutRequest(certsReloadEndpoint("aaaaa"), "{}"); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - rh.sendAdminCertificate = false; - response = rh.executePutRequest(certsReloadEndpoint("bbbb"), "{}", restApiAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - response = rh.executePutRequest(certsReloadEndpoint("cccc"), "{}", restApiReloadCertsAdminHeader); - Assert.assertEquals(response.getBody(), HttpStatus.SC_FORBIDDEN, response.getStatusCode()); - - } - - private void sendAdminCert() { - rh.keystore = "restapi/kirk-keystore.jks"; - rh.sendAdminCertificate = true; - } - - Settings reloadEnabled() { - return Settings.builder().put(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, true).build(); - } - -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/TenantInfoActionTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/TenantInfoActionTest.java index 2c6a45faf7..2e47aae556 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/TenantInfoActionTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/TenantInfoActionTest.java @@ -12,7 +12,7 @@ package org.opensearch.security.dlic.rest.api; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; 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 ca467801f0..2a9292b3a9 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 @@ -21,8 +21,8 @@ import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java index b9b3cf50b8..0e3d330b52 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/WhitelistApiTest.java @@ -17,7 +17,7 @@ import com.google.common.collect.ImmutableMap; import com.fasterxml.jackson.databind.JsonNode; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyAccountApiTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyAccountApiTests.java deleted file mode 100644 index 925d90ccba..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyAccountApiTests.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api.legacy; - -import org.opensearch.security.dlic.rest.api.AccountApiTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacyAccountApiTests extends AccountApiTest { - @Override - protected String getEndpointPrefix() { - return LEGACY_OPENDISTRO_PREFIX; - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyDashboardsInfoActionTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyDashboardsInfoActionTests.java deleted file mode 100644 index ee39f93ee0..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyDashboardsInfoActionTests.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api.legacy; - -import org.opensearch.security.dlic.rest.api.DashboardsInfoActionTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacyDashboardsInfoActionTests extends DashboardsInfoActionTest { - @Override - protected String getEndpoint() { - return LEGACY_OPENDISTRO_PREFIX + "/kibanainfo"; - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyFlushCacheApiTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyFlushCacheApiTests.java deleted file mode 100644 index df9cc3d59d..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacyFlushCacheApiTests.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api.legacy; - -import org.opensearch.security.dlic.rest.api.FlushCacheApiTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacyFlushCacheApiTests extends FlushCacheApiTest { - @Override - protected String getEndpointPrefix() { - return LEGACY_OPENDISTRO_PREFIX; - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityHealthActionTests.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityHealthActionTests.java deleted file mode 100644 index 99fa4a99ae..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySecurityHealthActionTests.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api.legacy; - -import org.opensearch.security.dlic.rest.api.SecurityHealthActionTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacySecurityHealthActionTests extends SecurityHealthActionTest { - @Override - protected String getEndpointPrefix() { - return LEGACY_OPENDISTRO_PREFIX; - } -} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java deleted file mode 100644 index 5d1c3ae538..0000000000 --- a/src/test/java/org/opensearch/security/dlic/rest/api/legacy/LegacySslCertsApiTest.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.dlic.rest.api.legacy; - -import org.opensearch.security.dlic.rest.api.SslCertsApiTest; - -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; - -public class LegacySslCertsApiTest extends SslCertsApiTest { - - @Override - public String certsInfoEndpoint() { - return LEGACY_OPENDISTRO_PREFIX + "/api/ssl/certs"; - } - - @Override - public String certsReloadEndpoint(String certType) { - return String.format("%s/api/ssl/%s/reloadcerts", LEGACY_OPENDISTRO_PREFIX, certType); - } -} diff --git a/src/test/java/org/opensearch/security/filter/SecurityResponseTests.java b/src/test/java/org/opensearch/security/filter/SecurityResponseTests.java new file mode 100644 index 0000000000..7735a8a7cd --- /dev/null +++ b/src/test/java/org/opensearch/security/filter/SecurityResponseTests.java @@ -0,0 +1,155 @@ +/* + * 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.List; +import java.util.Map; + +import org.apache.hc.core5.http.HttpHeaders; +import org.apache.http.HttpStatus; +import org.junit.Test; + +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; + +public class SecurityResponseTests { + + /** + * This test should check whether a basic constructor with the JSON content type is successfully converted to RestResponse + */ + @Test + public void testSecurityResponseHasSingleContentType() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_OK, null, "foo bar", XContentType.JSON.mediaType()); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + assertThat(restResponse.contentType(), equalTo(XContentType.JSON.mediaType())); + } + + /** + * This test should check whether adding a new HTTP Header for the content type takes the argument or the added header (should take arg.) + */ + @Test + public void testSecurityResponseMultipleContentTypesUsesPassed() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_OK, null, "foo bar", XContentType.JSON.mediaType()); + response.addHeader(HttpHeaders.CONTENT_TYPE, BytesRestResponse.TEXT_CONTENT_TYPE); + assertThat(response.getHeaders().get("Content-Type"), equalTo(List.of(BytesRestResponse.TEXT_CONTENT_TYPE))); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo(XContentType.JSON.mediaType())); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + } + + /** + * This test should check whether specifying no content type correctly uses plain text + */ + @Test + public void testSecurityResponseDefaultContentTypeIsText() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_OK, null, "foo bar"); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo(BytesRestResponse.TEXT_CONTENT_TYPE)); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + } + + /** + * This test checks whether adding a new ContentType header actually changes the converted content type header (it should not) + */ + @Test + public void testSecurityResponseSetHeaderContentTypeDoesNothing() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_OK, null, "foo bar"); + response.addHeader(HttpHeaders.CONTENT_TYPE, XContentType.JSON.mediaType()); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo(BytesRestResponse.TEXT_CONTENT_TYPE)); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + } + + /** + * This test should check whether adding a multiple new HTTP Headers for the content type takes the argument or the added header (should take arg.) + */ + @Test + public void testSecurityResponseAddMultipleContentTypeHeaders() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_OK, null, "foo bar", XContentType.JSON.mediaType()); + response.addHeader(HttpHeaders.CONTENT_TYPE, BytesRestResponse.TEXT_CONTENT_TYPE); + assertThat(response.getHeaders().get("Content-Type"), equalTo(List.of(BytesRestResponse.TEXT_CONTENT_TYPE))); + response.addHeader(HttpHeaders.CONTENT_TYPE, "newContentType"); + assertThat(response.getHeaders().get("Content-Type"), equalTo(List.of(BytesRestResponse.TEXT_CONTENT_TYPE, "newContentType"))); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + } + + /** + * This test confirms that fake content types work for conversion + */ + @Test + public void testSecurityResponseFakeContentTypeArgumentPasses() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_OK, null, "foo bar", "testType"); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo("testType")); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + } + + /** + * This test checks that types passed as part of the Headers parameter in the argument do not overwrite actual Content Type + */ + @Test + public void testSecurityResponseContentTypeInConstructorHeader() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_OK, Map.of("Content-Type", "testType"), "foo bar"); + assertThat(response.getHeaders().get("Content-Type"), equalTo(List.of("testType"))); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo(BytesRestResponse.TEXT_CONTENT_TYPE)); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + } + + /** + * This test confirms the same as above but with a conflicting content type arg + */ + @Test + public void testSecurityResponseContentTypeInConstructorHeaderConflicts() { + final SecurityResponse response = new SecurityResponse( + HttpStatus.SC_OK, + Map.of("Content-Type", "testType"), + "foo bar", + XContentType.JSON.mediaType() + ); + assertThat(response.getHeaders().get("Content-Type"), equalTo(List.of("testType"))); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo(XContentType.JSON.mediaType())); + assertThat(restResponse.status(), equalTo(RestStatus.OK)); + } + + /** + * This test should check whether unauthorized requests are converted properly + */ + @Test + public void testSecurityResponseUnauthorizedRequestWithPlainTextContentType() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_UNAUTHORIZED, null, "foo bar"); + response.addHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo(BytesRestResponse.TEXT_CONTENT_TYPE)); + assertThat(restResponse.status(), equalTo(RestStatus.UNAUTHORIZED)); + } + + /** + * This test should check whether forbidden requests are converted properly + */ + @Test + public void testSecurityResponseForbiddenRequestWithPlainTextContentType() { + final SecurityResponse response = new SecurityResponse(HttpStatus.SC_FORBIDDEN, null, "foo bar"); + response.addHeader(HttpHeaders.CONTENT_TYPE, "application/json"); + final RestResponse restResponse = response.asRestResponse(); + assertThat(restResponse.contentType(), equalTo(BytesRestResponse.TEXT_CONTENT_TYPE)); + assertThat(restResponse.status(), equalTo(RestStatus.FORBIDDEN)); + } +} diff --git a/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java b/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java index 5adcadb1f2..b46c5a6e32 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityRestFilterTests.java @@ -12,7 +12,7 @@ package org.opensearch.security.filter; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.security.dlic.rest.api.AbstractRestApiUnitTest; diff --git a/src/test/java/org/opensearch/security/filter/SecurityRestUtilsTests.java b/src/test/java/org/opensearch/security/filter/SecurityRestUtilsTests.java index 46b0e82f2a..50ae2157c9 100644 --- a/src/test/java/org/opensearch/security/filter/SecurityRestUtilsTests.java +++ b/src/test/java/org/opensearch/security/filter/SecurityRestUtilsTests.java @@ -1,3 +1,13 @@ +/* + * 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.filter; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java b/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java index 3da6ad3d7f..6a8db35d14 100644 --- a/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java +++ b/src/test/java/org/opensearch/security/httpclient/HttpClientTest.java @@ -30,7 +30,10 @@ protected String getResourceFolder() { @Test public void testPlainConnection() throws Exception { - final Settings settings = Settings.builder().put("plugins.security.ssl.http.enabled", false).build(); + final Settings settings = Settings.builder() + .put("plugins.security.ssl.http.enabled", false) + .loadFromPath(FileHelper.getAbsoluteFilePathFromClassPath("auditlog/endpoints/routing/configuration_valid.yml")) + .build(); setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); @@ -84,6 +87,7 @@ public void testSslConnection() throws Exception { .put(SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_ALIAS, "node-0") .put("plugins.security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("auditlog/node-0-keystore.jks")) .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")) + .loadFromPath(FileHelper.getAbsoluteFilePathFromClassPath("auditlog/endpoints/routing/configuration_valid.yml")) .build(); setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); @@ -123,6 +127,7 @@ public void testSslConnectionPKIAuth() throws Exception { .put(SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_ALIAS, "node-0") .put("plugins.security.ssl.http.keystore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("auditlog/node-0-keystore.jks")) .put("plugins.security.ssl.http.truststore_filepath", FileHelper.getAbsoluteFilePathFromClassPath("auditlog/truststore.jks")) + .loadFromPath(FileHelper.getAbsoluteFilePathFromClassPath("auditlog/endpoints/routing/configuration_valid.yml")) .build(); setup(Settings.EMPTY, new DynamicSecurityConfig(), settings); diff --git a/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java b/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java index b66902f4b9..0a785d7b80 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/MultitenancyTests.java @@ -14,8 +14,8 @@ import java.util.Map; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java b/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java index b25a50d934..32b9bb2156 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/TenancyMultitenancyEnabledTests.java @@ -12,8 +12,8 @@ package org.opensearch.security.multitenancy.test; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.common.settings.Settings; diff --git a/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java b/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java index 1af102802f..4f2d2c3505 100644 --- a/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java +++ b/src/test/java/org/opensearch/security/multitenancy/test/TenancyPrivateTenantEnabledTests.java @@ -12,8 +12,8 @@ package org.opensearch.security.multitenancy.test; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.common.settings.Settings; diff --git a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java index 4f25c71d66..d5a26024a9 100644 --- a/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java +++ b/src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java @@ -12,7 +12,7 @@ package org.opensearch.security.privileges; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Assert; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java b/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java index bc4cc18f61..c1198269b1 100644 --- a/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java +++ b/src/test/java/org/opensearch/security/protected_indices/ProtectedIndicesTests.java @@ -31,7 +31,7 @@ import java.util.List; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Test; import org.opensearch.action.admin.cluster.repositories.put.PutRepositoryRequest; diff --git a/src/test/java/org/opensearch/security/sanity/tests/InvalidAdminPasswordIT.java b/src/test/java/org/opensearch/security/sanity/tests/InvalidAdminPasswordIT.java new file mode 100644 index 0000000000..60d2eee138 --- /dev/null +++ b/src/test/java/org/opensearch/security/sanity/tests/InvalidAdminPasswordIT.java @@ -0,0 +1,50 @@ +/* + * 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.sanity.tests; + +import org.hamcrest.MatcherAssert; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.ResponseException; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class InvalidAdminPasswordIT extends SecurityRestTestCase { + + static String currentPasswordVariable = System.getProperty("password"); + + @BeforeClass + public static void setUpAdminAsPasswordVariable() { + System.setProperty("password", "admin"); + } + + @AfterClass + public static void restorePasswordProperty() { + System.setProperty("password", currentPasswordVariable); + } + + @Test + public void testAdminCredentials_adminAsPassword_shouldFail() throws Exception { + try { + client().performRequest(new Request("GET", "")); + } catch (ResponseException e) { + Response res = e.getResponse(); + MatcherAssert.assertThat(res.getStatusLine().getStatusCode(), is(equalTo(401))); + MatcherAssert.assertThat(res.getStatusLine().getReasonPhrase(), is(equalTo("Unauthorized"))); + } + } +} diff --git a/src/test/java/org/opensearch/security/sanity/tests/SingleClusterSanityIT.java b/src/test/java/org/opensearch/security/sanity/tests/SingleClusterSanityIT.java index 8987744d58..97937a2c52 100644 --- a/src/test/java/org/opensearch/security/sanity/tests/SingleClusterSanityIT.java +++ b/src/test/java/org/opensearch/security/sanity/tests/SingleClusterSanityIT.java @@ -19,7 +19,11 @@ import org.hamcrest.MatcherAssert; import org.junit.Test; +import org.opensearch.client.Request; +import org.opensearch.client.Response; + import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; @@ -34,6 +38,13 @@ public void testSecurityPluginInstallation() throws Exception { verifyPluginInstallationOnAllNodes(); } + @Test + public void testAdminCredentials_validAdminPassword_shouldSucceed() throws Exception { + Response response = client().performRequest(new Request("GET", "")); + MatcherAssert.assertThat(response.getStatusLine().getStatusCode(), is(equalTo(200))); + MatcherAssert.assertThat(response.getStatusLine().getReasonPhrase(), is(equalTo("OK"))); + } + private void verifyPluginInstallationOnAllNodes() throws Exception { Map> nodesInCluster = (Map>) getAsMapByAdmin("_nodes").get("nodes"); diff --git a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java index ace182bcda..530db3211b 100644 --- a/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java +++ b/src/test/java/org/opensearch/security/securityconf/SecurityRolesPermissionsV6Test.java @@ -126,6 +126,17 @@ public void hasExplicitIndexPermission() { ); } + @Test + public void isPermittedOnSystemIndex() { + final SecurityRoles securityRoleWithExplicitAccess = configModel.getSecurityRoles() + .filter(ImmutableSet.of("has_system_index_permission")); + Assert.assertTrue(securityRoleWithExplicitAccess.isPermittedOnSystemIndex(TEST_INDEX)); + + final SecurityRoles securityRoleWithStarAccess = configModel.getSecurityRoles() + .filter(ImmutableSet.of("all_access_without_system_index_permission")); + Assert.assertFalse(securityRoleWithStarAccess.isPermittedOnSystemIndex(TEST_INDEX)); + } + static SecurityDynamicConfiguration createRolesConfig() throws IOException { final ObjectNode rolesNode = DefaultObjectMapper.objectMapper.createObjectNode(); NO_EXPLICIT_SYSTEM_INDEX_PERMISSION.forEach(rolesNode::set); diff --git a/src/test/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfigurationTest.java b/src/test/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfigurationTest.java new file mode 100644 index 0000000000..c554784581 --- /dev/null +++ b/src/test/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfigurationTest.java @@ -0,0 +1,52 @@ +/* + * 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.securityconf.impl; + +import java.io.IOException; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.security.DefaultObjectMapper; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +public class SecurityDynamicConfigurationTest { + + private SecurityDynamicConfiguration securityDynamicConfiguration; + private ObjectMapper objectMapper = DefaultObjectMapper.objectMapper; + private ObjectNode objectNode = objectMapper.createObjectNode(); + + @Before + public void setUp() throws JsonProcessingException, IOException { + objectNode.set("_meta", objectMapper.createObjectNode().put("type", CType.ROLES.toLCString()).put("config_version", 2)); + securityDynamicConfiguration = SecurityDynamicConfiguration.fromJson( + objectMapper.writeValueAsString(objectNode), + CType.ROLES, + 2, + 1, + 1 + ); + } + + @Test + public void deepClone_shouldReturnNewObject() { + SecurityDynamicConfiguration securityDeepClone = securityDynamicConfiguration.deepClone(); + assertThat(securityDeepClone, is(not(equalTo(securityDynamicConfiguration)))); + } +} diff --git a/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java b/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java index 2983fc6064..a780b0066f 100644 --- a/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java +++ b/src/test/java/org/opensearch/security/securityconf/impl/v6/ConfigV6Test.java @@ -20,6 +20,10 @@ import org.opensearch.security.DefaultObjectMapper; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + @RunWith(Parameterized.class) public class ConfigV6Test { private final boolean omitDefaults; @@ -31,6 +35,9 @@ public static Iterable omitDefaults() { public void assertEquals(ConfigV6.Kibana expected, JsonNode node) { Assert.assertEquals(expected.multitenancy_enabled, node.get("multitenancy_enabled").asBoolean()); + assertThat(node.get("sign_in_options").isArray(), is(true)); + assertThat(node.get("sign_in_options").toString(), containsString(expected.sign_in_options.get(0).toString())); + if (expected.server_username == null) { Assert.assertNull(node.get("server_username")); } else { @@ -57,6 +64,7 @@ public void assertEquals(ConfigV6.Kibana expected, JsonNode node) { private void assertEquals(ConfigV6.Kibana expected, ConfigV6.Kibana actual) { Assert.assertEquals(expected.multitenancy_enabled, actual.multitenancy_enabled); + assertThat(expected.sign_in_options, is(actual.sign_in_options)); if (expected.server_username == null) { // null is restored to default instead of null Assert.assertEquals(new ConfigV6.Kibana().server_username, actual.server_username); diff --git a/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java b/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java index 542ce878bd..246247c6d9 100644 --- a/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java +++ b/src/test/java/org/opensearch/security/securityconf/impl/v7/ConfigV7Test.java @@ -20,6 +20,10 @@ import org.opensearch.security.DefaultObjectMapper; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; + @RunWith(Parameterized.class) public class ConfigV7Test { private final boolean omitDefaults; @@ -31,6 +35,9 @@ public static Iterable omitDefaults() { public void assertEquals(ConfigV7.Kibana expected, JsonNode node) { Assert.assertEquals(expected.multitenancy_enabled, node.get("multitenancy_enabled").asBoolean()); + assertThat(node.get("sign_in_options").isArray(), is(true)); + assertThat(node.get("sign_in_options").toString(), containsString(expected.sign_in_options.get(0).toString())); + if (expected.server_username == null) { Assert.assertNull(node.get("server_username")); } else { @@ -51,6 +58,7 @@ public void assertEquals(ConfigV7.Kibana expected, JsonNode node) { private void assertEquals(ConfigV7.Kibana expected, ConfigV7.Kibana actual) { Assert.assertEquals(expected.multitenancy_enabled, actual.multitenancy_enabled); + assertThat(expected.sign_in_options, is(actual.sign_in_options)); if (expected.server_username == null) { // null is restored to default instead of null Assert.assertEquals(new ConfigV7.Kibana().server_username, actual.server_username); diff --git a/src/test/java/org/opensearch/security/setting/DeprecatedSettingsTest.java b/src/test/java/org/opensearch/security/setting/DeprecatedSettingsTest.java index a0f9558228..3fa8e45816 100644 --- a/src/test/java/org/opensearch/security/setting/DeprecatedSettingsTest.java +++ b/src/test/java/org/opensearch/security/setting/DeprecatedSettingsTest.java @@ -5,17 +5,22 @@ package org.opensearch.security.setting; +import com.fasterxml.jackson.databind.JsonMappingException; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.opensearch.Version; import org.opensearch.common.logging.DeprecationLogger; import org.opensearch.common.settings.Settings; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.support.ConfigHelper; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -61,4 +66,96 @@ public void testCheckForDeprecatedSettingFoundLegacy() { verify(logger).deprecate(eq("legacyKey"), anyString(), any(), any()); } + + @Test + public void testForTransportEnabledDeprecationMessageOnYamlLoad() throws Exception { + ConfigHelper.fromYamlString( + "---\n" + + "_meta:\n" + + " type: \"config\"\n" + + " config_version: 2\n" + + "config:\n" + + " dynamic:\n" + + " authc:\n" + + " authentication_domain_kerb:\n" + + " http_enabled: false\n" + + " transport_enabled: false\n" + + " order: 3\n" + + " http_authenticator:\n" + + " challenge: true\n" + + " type: \"kerberos\"\n" + + " config: {}\n" + + " authentication_backend:\n" + + " type: \"noop\"\n" + + " config: {}\n" + + " description: \"Migrated from v6\"\n" + + " authz:\n" + + " roles_from_xxx:\n" + + " http_enabled: false\n" + + " transport_enabled: false\n" + + " authorization_backend:\n" + + " type: \"xxx\"\n" + + " config: {}\n" + + " description: \"Migrated from v6\"", + CType.CONFIG, + DEFAULT_CONFIG_VERSION, + 0, + 0 + ); + verify(logger).deprecate( + "transport_enabled", + "In OpenSearch " + + Version.CURRENT + + " the setting '{}' is deprecated, it should be removed from the relevant config file using the following location information: In AuthcDomain, using http_authenticator=HttpAuthenticator [challenge=true, type=null, config={}], authentication_backend=AuthcBackend [type=org.opensearch.security.auth.internal.InternalAuthenticationBackend, config={}]", + "transport_enabled" + ); + verify(logger).deprecate( + "transport_enabled", + "In OpenSearch " + + Version.CURRENT + + " the setting '{}' is deprecated, it should be removed from the relevant config file using the following location information: In AuthzDomain, using authorization_backend=AuthzBackend [type=noop, config={}]", + "transport_enabled" + ); + } + + @Test + public void testForExceptionOnUnknownAuthcAuthzSettingsOnYamlLoad() throws Exception { + try { + ConfigHelper.fromYamlString( + "---\n" + + "_meta:\n" + + " type: \"config\"\n" + + " config_version: 2\n" + + "config:\n" + + " dynamic:\n" + + " authc:\n" + + " authentication_domain_kerb:\n" + + " http_enabled: false\n" + + " unknown_property: false\n" + + " order: 3\n" + + " http_authenticator:\n" + + " challenge: true\n" + + " type: \"kerberos\"\n" + + " config: {}\n" + + " authentication_backend:\n" + + " type: \"noop\"\n" + + " config: {}\n" + + " description: \"Migrated from v6\"\n" + + " authz:\n" + + " roles_from_xxx:\n" + + " http_enabled: false\n" + + " unknown_property: false\n" + + " authorization_backend:\n" + + " type: \"xxx\"\n" + + " config: {}\n" + + " description: \"Migrated from v6\"", + CType.CONFIG, + DEFAULT_CONFIG_VERSION, + 0, + 0 + ); + } catch (JsonMappingException e) { + verifyNoInteractions(logger); + } + } } diff --git a/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java new file mode 100644 index 0000000000..aefb12c0db --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/OpenSearchSecuritySSLPluginTest.java @@ -0,0 +1,326 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.ssl; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLException; + +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.OpenSearchException; +import org.opensearch.common.collect.Tuple; +import org.opensearch.common.network.NetworkModule; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.http.HttpServerTransport; +import org.opensearch.http.netty4.ssl.SecureNetty4HttpServerTransport; +import org.opensearch.plugins.SecureHttpTransportSettingsProvider; +import org.opensearch.plugins.SecureTransportSettingsProvider; +import org.opensearch.plugins.TransportExceptionHandler; +import org.opensearch.security.ssl.util.SSLConfigConstants; +import org.opensearch.security.support.SecuritySettings; +import org.opensearch.security.test.AbstractSecurityUnitTest; +import org.opensearch.security.test.helper.file.FileHelper; +import org.opensearch.telemetry.tracing.noop.NoopTracer; +import org.opensearch.transport.Transport; +import org.opensearch.transport.TransportAdapterProvider; + +import io.netty.channel.ChannelInboundHandlerAdapter; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.CoreMatchers.nullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsMapContaining.hasKey; +import static org.junit.Assert.assertThrows; + +public class OpenSearchSecuritySSLPluginTest extends AbstractSecurityUnitTest { + private Settings settings; + private SecureHttpTransportSettingsProvider secureHttpTransportSettingsProvider; + private SecureTransportSettingsProvider secureTransportSettingsProvider; + private ClusterSettings clusterSettings; + + @Before + public void setUp() { + settings = Settings.builder() + .put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks") + ) + .put( + SSLConfigConstants.SECURITY_SSL_HTTP_PEMTRUSTEDCAS_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/root-ca.pem") + ) + .put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/truststore.jks") + ) + .put( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/kirk-keystore.jks") + ) + .put( + SSLConfigConstants.SECURITY_SSL_HTTP_KEYSTORE_FILEPATH, + FileHelper.getAbsoluteFilePathFromClassPath("ssl/node-0-keystore.jks") + ) + .put(SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED, true) + .put(OpenSearchSecuritySSLPlugin.CLIENT_TYPE, "node") + .build(); + + secureTransportSettingsProvider = new SecureTransportSettingsProvider() { + @Override + public Optional buildServerTransportExceptionHandler(Settings settings, Transport transport) { + return Optional.empty(); + } + + @Override + public Optional buildSecureServerTransportEngine(Settings settings, Transport transport) throws SSLException { + return Optional.empty(); + } + + @Override + public Optional buildSecureClientTransportEngine(Settings settings, String hostname, int port) throws SSLException { + return Optional.empty(); + } + }; + + secureHttpTransportSettingsProvider = new SecureHttpTransportSettingsProvider() { + @Override + public Optional buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) { + return Optional.empty(); + } + + @Override + public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { + return Optional.empty(); + } + }; + + clusterSettings = new ClusterSettings(Settings.EMPTY, ClusterSettings.BUILT_IN_CLUSTER_SETTINGS); + } + + @Test + public void testRegisterSecureHttpTransport() throws IOException { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + final Map> transports = plugin.getSecureHttpTransports( + settings, + MOCK_POOL, + null, + null, + null, + null, + null, + null, + clusterSettings, + secureHttpTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport")); + assertThat( + transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport").get(), + not(nullValue()) + ); + } + } + + @Test + public void testRegisterSecureTransport() throws IOException { + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + final Map> transports = plugin.getSecureTransports( + settings, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + assertThat(transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport").get(), not(nullValue())); + } + } + + @Test + public void testRegisterSecureTransportWithDeprecatedSecuirtyPluginSettings() throws IOException { + final Settings deprecated = Settings.builder() + .put(settings) + .put(SecuritySettings.SSL_DUAL_MODE_SETTING.getKey(), true) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, false) + .put(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, false) + .build(); + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(deprecated, null, false)) { + final Map> transports = plugin.getSecureTransports( + deprecated, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + assertThat(transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport").get(), not(nullValue())); + } + } + + @Test + public void testRegisterSecureTransportWithNetworkModuleSettings() throws IOException { + final Settings migrated = Settings.builder() + .put(settings) + .put(NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY, true) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY, false) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) + .build(); + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + final Map> transports = plugin.getSecureTransports( + migrated, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + assertThat(transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport").get(), not(nullValue())); + } + } + + @Test + public void testRegisterSecureTransportWithDuplicateSettings() throws IOException { + final Collection> duplicates = List.of( + Tuple.tuple(SecuritySettings.SSL_DUAL_MODE_SETTING.getKey(), NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY), + Tuple.tuple( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME, + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY + ), + Tuple.tuple( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENFORCE_HOSTNAME_VERIFICATION, + NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY + ) + ); + + for (final Tuple duplicate : duplicates) { + final Settings migrated = Settings.builder() + .put(settings) + .put(duplicate.v1(), true) + .put(NetworkModule.TRANSPORT_SSL_DUAL_MODE_ENABLED_KEY, true) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_RESOLVE_HOST_NAME_KEY, false) + .put(NetworkModule.TRANSPORT_SSL_ENFORCE_HOSTNAME_VERIFICATION_KEY, false) + .build(); + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(migrated, null, false)) { + final Map> transports = plugin.getSecureTransports( + migrated, + MOCK_POOL, + null, + null, + null, + null, + secureTransportSettingsProvider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")); + final OpenSearchException ex = assertThrows( + OpenSearchException.class, + transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport")::get + ); + assertThat( + ex.getMessage(), + containsString( + "Only one of the settings [" + + duplicate.v2() + + ", " + + duplicate.v1() + + " (deprecated)] could be specified but not both" + ) + ); + } + } + } + + @Test + public void testRegisterSecureHttpTransportWithRequestHeaderVerifier() throws IOException { + final AtomicBoolean created = new AtomicBoolean(false); + + class LocalHeaderVerifier extends ChannelInboundHandlerAdapter { + public LocalHeaderVerifier() { + created.set(true); + } + } + + final SecureHttpTransportSettingsProvider provider = new SecureHttpTransportSettingsProvider() { + @Override + public Collection> getHttpTransportAdapterProviders(Settings settings) { + return List.of(new TransportAdapterProvider() { + + @Override + public String name() { + return SecureNetty4HttpServerTransport.REQUEST_HEADER_VERIFIER; + } + + @SuppressWarnings("unchecked") + @Override + public Optional create(Settings settings, HttpServerTransport transport, Class adapterClass) { + return Optional.of((C) new LocalHeaderVerifier()); + } + + }); + } + + @Override + public Optional buildHttpServerExceptionHandler(Settings settings, HttpServerTransport transport) { + return Optional.empty(); + } + + @Override + public Optional buildSecureHttpServerEngine(Settings settings, HttpServerTransport transport) throws SSLException { + return Optional.empty(); + } + }; + + try (OpenSearchSecuritySSLPlugin plugin = new OpenSearchSecuritySSLPlugin(settings, null, false)) { + final Map> transports = plugin.getSecureHttpTransports( + settings, + MOCK_POOL, + null, + null, + null, + null, + null, + null, + clusterSettings, + provider, + NoopTracer.INSTANCE + ); + assertThat(transports, hasKey("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport")); + + assertThat( + transports.get("org.opensearch.security.ssl.http.netty.SecuritySSLNettyHttpServerTransport").get(), + not(nullValue()) + ); + + assertThat(created.get(), is(true)); + } + } +} diff --git a/src/test/java/org/opensearch/security/ssl/transport/DualModeSSLHandlerTests.java b/src/test/java/org/opensearch/security/ssl/transport/DualModeSSLHandlerTests.java deleted file mode 100644 index e71e77d414..0000000000 --- a/src/test/java/org/opensearch/security/ssl/transport/DualModeSSLHandlerTests.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ -package org.opensearch.security.ssl.transport; - -import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; - -import org.opensearch.security.ssl.SecurityKeyStore; -import org.opensearch.security.ssl.util.SSLConnectionTestUtil; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.channel.ChannelFuture; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelPipeline; -import io.netty.handler.ssl.SslHandler; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import static org.opensearch.transport.NettyAllocator.getAllocator; - -public class DualModeSSLHandlerTests { - - public static final int TLS_MAJOR_VERSION = 3; - public static final int TLS_MINOR_VERSION = 0; - private static final ByteBufAllocator ALLOCATOR = getAllocator(); - - private SecurityKeyStore securityKeyStore; - private ChannelPipeline pipeline; - private ChannelHandlerContext ctx; - private SslHandler sslHandler; - - @Before - public void setup() { - pipeline = Mockito.mock(ChannelPipeline.class); - ctx = Mockito.mock(ChannelHandlerContext.class); - Mockito.when(ctx.pipeline()).thenReturn(pipeline); - - securityKeyStore = Mockito.mock(SecurityKeyStore.class); - sslHandler = Mockito.mock(SslHandler.class); - } - - @Test - public void testInvalidMessage() throws Exception { - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore); - - handler.decode(ctx, ALLOCATOR.buffer(4), null); - // ensure pipeline is not fetched and manipulated - Mockito.verify(ctx, Mockito.times(0)).pipeline(); - } - - @Test - public void testValidTLSMessage() throws Exception { - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore, sslHandler); - - ByteBuf buffer = ALLOCATOR.buffer(6); - buffer.writeByte(20); - buffer.writeByte(TLS_MAJOR_VERSION); - buffer.writeByte(TLS_MINOR_VERSION); - buffer.writeByte(100); - buffer.writeByte(0); - buffer.writeByte(0); - - handler.decode(ctx, buffer, null); - // ensure ssl handler is added - Mockito.verify(ctx, Mockito.times(1)).pipeline(); - Mockito.verify(pipeline, Mockito.times(1)).addAfter("port_unification_handler", "ssl_server", sslHandler); - Mockito.verify(pipeline, Mockito.times(1)).remove(handler); - } - - @Test - public void testNonTLSMessage() throws Exception { - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore, sslHandler); - - ByteBuf buffer = ALLOCATOR.buffer(6); - - for (int i = 0; i < 6; i++) { - buffer.writeByte(1); - } - - handler.decode(ctx, buffer, null); - // ensure ssl handler is added - Mockito.verify(ctx, Mockito.times(1)).pipeline(); - Mockito.verify(pipeline, Mockito.times(0)).addAfter("port_unification_handler", "ssl_server", sslHandler); - Mockito.verify(pipeline, Mockito.times(1)).remove(handler); - } - - @Test - public void testDualModeClientHelloMessage() throws Exception { - ChannelFuture channelFuture = Mockito.mock(ChannelFuture.class); - Mockito.when(ctx.writeAndFlush(Mockito.any())).thenReturn(channelFuture); - Mockito.when(channelFuture.addListener(Mockito.any())).thenReturn(channelFuture); - - ByteBuf buffer = ALLOCATOR.buffer(6); - buffer.writeCharSequence(SSLConnectionTestUtil.DUAL_MODE_CLIENT_HELLO_MSG, StandardCharsets.UTF_8); - - DualModeSSLHandler handler = new DualModeSSLHandler(securityKeyStore, sslHandler); - List decodedObjs = new ArrayList<>(); - handler.decode(ctx, buffer, decodedObjs); - - ArgumentCaptor serverHelloReplyBuffer = ArgumentCaptor.forClass(ByteBuf.class); - Mockito.verify(ctx, Mockito.times(1)).writeAndFlush(serverHelloReplyBuffer.capture()); - - String actualReply = serverHelloReplyBuffer.getValue().getCharSequence(0, 6, StandardCharsets.UTF_8).toString(); - Assert.assertEquals(SSLConnectionTestUtil.DUAL_MODE_SERVER_HELLO_MSG, actualReply); - } -} diff --git a/src/test/java/org/opensearch/security/ssl/util/SSLConfigConstantsTest.java b/src/test/java/org/opensearch/security/ssl/util/SSLConfigConstantsTest.java new file mode 100644 index 0000000000..b51efeda03 --- /dev/null +++ b/src/test/java/org/opensearch/security/ssl/util/SSLConfigConstantsTest.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.ssl.util; + +import java.util.List; + +import org.junit.Test; + +import org.opensearch.common.settings.Settings; + +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_HTTP_ENABLED_PROTOCOLS; +import static org.opensearch.security.ssl.util.SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS; +import static org.junit.Assert.assertArrayEquals; + +public class SSLConfigConstantsTest { + + @Test + public void testDefaultTLSProtocols() { + final var tlsDefaultProtocols = SSLConfigConstants.getSecureSSLProtocols(Settings.EMPTY, false); + assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1" }, tlsDefaultProtocols); + } + + @Test + public void testDefaultSSLProtocols() { + final var sslDefaultProtocols = SSLConfigConstants.getSecureSSLProtocols(Settings.EMPTY, true); + assertArrayEquals(new String[] { "TLSv1.3", "TLSv1.2", "TLSv1.1" }, sslDefaultProtocols); + } + + @Test + public void testCustomTLSProtocols() { + final var tlsDefaultProtocols = SSLConfigConstants.getSecureSSLProtocols( + Settings.builder().putList(SECURITY_SSL_TRANSPORT_ENABLED_PROTOCOLS, List.of("TLSv1", "TLSv1.1")).build(), + false + ); + assertArrayEquals(new String[] { "TLSv1", "TLSv1.1" }, tlsDefaultProtocols); + } + + @Test + public void testCustomSSLProtocols() { + final var sslDefaultProtocols = SSLConfigConstants.getSecureSSLProtocols( + Settings.builder().putList(SECURITY_SSL_HTTP_ENABLED_PROTOCOLS, List.of("TLSv1", "TLSv1.1")).build(), + true + ); + assertArrayEquals(new String[] { "TLSv1", "TLSv1.1" }, sslDefaultProtocols); + } + +} diff --git a/src/test/java/org/opensearch/security/state/SecurityMetadataSerializationTestCase.java b/src/test/java/org/opensearch/security/state/SecurityMetadataSerializationTestCase.java new file mode 100644 index 0000000000..c52f37cf54 --- /dev/null +++ b/src/test/java/org/opensearch/security/state/SecurityMetadataSerializationTestCase.java @@ -0,0 +1,154 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ +package org.opensearch.security.state; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import com.carrotsearch.randomizedtesting.RandomizedContext; +import com.carrotsearch.randomizedtesting.RandomizedRunner; +import com.carrotsearch.randomizedtesting.RandomizedTest; +import com.google.common.collect.ImmutableSortedSet; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.Version; +import org.opensearch.cluster.ClusterState; +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.core.common.io.stream.NamedWriteableAwareStreamInput; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.test.DiffableTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotSame; + +@RunWith(RandomizedRunner.class) +public class SecurityMetadataSerializationTestCase extends RandomizedTest { + + protected ClusterState.Custom createTestInstance() { + final var configuration = new ImmutableSortedSet.Builder<>(Comparator.comparing(SecurityConfig::type)); + for (final var c : CType.values()) { + configuration.add(new SecurityConfig(c, randomAsciiAlphanumOfLength(128), null)); + } + return new SecurityMetadata(randomInstant(), configuration.build()); + } + + protected ClusterState.Custom makeTestChanges(ClusterState.Custom custom) { + final var securityMetadata = (SecurityMetadata) custom; + + if (randomBoolean()) { + final var configuration = securityMetadata.configuration(); + int leaveElements = randomIntBetween(0, configuration.size() - 1); + final var randomConfigs = randomSubsetOf(leaveElements, configuration); + final var securityMetadataBuilder = SecurityMetadata.from(securityMetadata); + for (final var config : randomConfigs) { + securityMetadataBuilder.withSecurityConfig( + SecurityConfig.from(config).withLastModified(randomInstant()).withHash(randomAsciiAlphanumOfLength(128)).build() + ); + } + return securityMetadataBuilder.build(); + } + + return securityMetadata; + } + + public static List randomSubsetOf(int size, Collection collection) { + if (size > collection.size()) { + throw new IllegalArgumentException( + "Can't pick " + size + " random objects from a collection of " + collection.size() + " objects" + ); + } + List tempList = new ArrayList<>(collection); + Collections.shuffle(tempList, RandomizedContext.current().getRandom()); + return tempList.subList(0, size); + } + + protected Instant randomInstant() { + return Instant.ofEpochSecond(randomLongBetween(0L, 3000000000L), randomLongBetween(0L, 999999999L)); + } + + @Test + public void testSerialization() throws IOException { + for (int runs = 0; runs < 20; runs++) { + ClusterState.Custom testInstance = createTestInstance(); + assertSerialization(testInstance); + } + } + + void assertSerialization(ClusterState.Custom testInstance) throws IOException { + assertSerialization(testInstance, Version.CURRENT); + } + + void assertSerialization(ClusterState.Custom testInstance, Version version) throws IOException { + ClusterState.Custom deserializedInstance = copyInstance(testInstance, version); + assertEqualInstances(testInstance, deserializedInstance); + } + + void assertEqualInstances(ClusterState.Custom expectedInstance, ClusterState.Custom newInstance) { + assertNotSame(newInstance, expectedInstance); + assertEquals(expectedInstance, newInstance); + assertEquals(expectedInstance.hashCode(), newInstance.hashCode()); + } + + @Test + public void testDiffableSerialization() throws IOException { + DiffableTestUtils.testDiffableSerialization( + this::createTestInstance, + this::makeTestChanges, + getNamedWriteableRegistry(), + SecurityMetadata::new, + SecurityMetadata::readDiffFrom + ); + } + + protected NamedWriteableRegistry getNamedWriteableRegistry() { + return new NamedWriteableRegistry(Collections.emptyList()); + } + + protected final ClusterState.Custom copyInstance(ClusterState.Custom instance, Version version) throws IOException { + return copyWriteable(instance, getNamedWriteableRegistry(), SecurityMetadata::new, version); + } + + public static T copyWriteable( + T original, + NamedWriteableRegistry namedWriteableRegistry, + Writeable.Reader reader, + Version version + ) throws IOException { + return copyInstance(original, namedWriteableRegistry, (out, value) -> value.writeTo(out), reader, version); + } + + protected static T copyInstance( + T original, + NamedWriteableRegistry namedWriteableRegistry, + Writeable.Writer writer, + Writeable.Reader reader, + Version version + ) throws IOException { + try (BytesStreamOutput output = new BytesStreamOutput()) { + output.setVersion(version); + writer.write(output, original); + try (StreamInput in = new NamedWriteableAwareStreamInput(output.bytes().streamInput(), namedWriteableRegistry)) { + in.setVersion(version); + return reader.read(in); + } + } + } + +} diff --git a/src/test/java/org/opensearch/security/support/Base64HelperTest.java b/src/test/java/org/opensearch/security/support/Base64HelperTest.java index 3bc81aaebc..32d96767d8 100644 --- a/src/test/java/org/opensearch/security/support/Base64HelperTest.java +++ b/src/test/java/org/opensearch/security/support/Base64HelperTest.java @@ -11,12 +11,17 @@ package org.opensearch.security.support; import java.io.Serializable; +import java.util.HashMap; +import java.util.stream.IntStream; import org.junit.Assert; import org.junit.Test; +import static org.hamcrest.Matchers.closeTo; +import static org.hamcrest.Matchers.equalTo; import static org.opensearch.security.support.Base64Helper.deserializeObject; import static org.opensearch.security.support.Base64Helper.serializeObject; +import static org.junit.Assert.assertThat; public class Base64HelperTest { @@ -48,4 +53,22 @@ public void testEnsureJDKSerialized() { Assert.assertEquals(jdkSerialized, Base64Helper.ensureJDKSerialized(jdkSerialized)); Assert.assertEquals(jdkSerialized, Base64Helper.ensureJDKSerialized(customSerialized)); } + + @Test + public void testDuplicatedItemSizes() { + var largeObject = new HashMap(); + var hm = new HashMap<>(); + IntStream.range(0, 100).forEach(i -> { hm.put("c" + i, "cvalue" + i); }); + IntStream.range(0, 100).forEach(i -> { largeObject.put("b" + i, hm); }); + + final var jdkSerialized = Base64Helper.serializeObject(largeObject, true); + final var customSerialized = Base64Helper.serializeObject(largeObject, false); + final var customSerializedOnlyHashMap = Base64Helper.serializeObject(hm, false); + + assertThat(jdkSerialized.length(), equalTo(3832)); + // The custom serializer is ~50x larger than the jdk serialized version + assertThat(customSerialized.length(), equalTo(184792)); + // Show that the majority of the size of the custom serialized large object is the map duplicated ~100 times + assertThat((double) customSerializedOnlyHashMap.length(), closeTo(customSerialized.length() / 100, 70d)); + } } diff --git a/src/test/java/org/opensearch/security/support/ConfigReaderTest.java b/src/test/java/org/opensearch/security/support/ConfigReaderTest.java new file mode 100644 index 0000000000..189b92ff68 --- /dev/null +++ b/src/test/java/org/opensearch/security/support/ConfigReaderTest.java @@ -0,0 +1,63 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.support; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; + +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; + +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +public class ConfigReaderTest { + + @ClassRule + public static TemporaryFolder folder = new TemporaryFolder(); + + private static File configDir; + + @BeforeClass + public static void createConfigFile() throws IOException { + configDir = folder.newFolder("config"); + } + + @Test + public void testThrowsIOExceptionForMandatoryCTypes() { + for (final var cType : CType.REQUIRED_CONFIG_FILES) { + assertThrows(IOException.class, () -> YamlConfigReader.newReader(cType, configDir.toPath())); + } + } + + @Test + public void testCreateReaderForNonMandatoryCTypes() throws IOException { + final var yamlMapper = DefaultObjectMapper.YAML_MAPPER; + for (final var cType : CType.NOT_REQUIRED_CONFIG_FILES) { + try (final var reader = new BufferedReader(YamlConfigReader.newReader(cType, configDir.toPath()))) { + final var emptyYaml = yamlMapper.readTree(reader); + assertTrue(emptyYaml.has("_meta")); + + final var meta = emptyYaml.get("_meta"); + assertEquals(cType.toLCString(), meta.get("type").asText()); + assertEquals(DEFAULT_CONFIG_VERSION, meta.get("config_version").asInt()); + } + } + } + +} diff --git a/src/test/java/org/opensearch/security/support/JsonFlattenerTest.java b/src/test/java/org/opensearch/security/support/JsonFlattenerTest.java new file mode 100644 index 0000000000..2880de387b --- /dev/null +++ b/src/test/java/org/opensearch/security/support/JsonFlattenerTest.java @@ -0,0 +1,404 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ + +package org.opensearch.security.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; + +public class JsonFlattenerTest { + @Test + public void testFlattenAsMapBasic() { + Map flattenedMap1 = JsonFlattener.flattenAsMap("{\"key\": {\"nested\": 1}, \"another.key\": [\"one\", \"two\"] }"); + assertThat(flattenedMap1.keySet(), containsInAnyOrder("key.nested", "key", "another.key[0]", "another.key[1]", "another.key")); + assertThat( + flattenedMap1.values(), + containsInAnyOrder(1, "one", "two", Arrays.asList("one", "two"), Collections.singletonMap("nested", 1)) + ); + Map flattenedMap2 = JsonFlattener.flattenAsMap("{\"a\":1, \"b\":2, \"cn\":{\"c\":[3,4]}}"); + assertThat(flattenedMap2.keySet(), containsInAnyOrder("a", "b", "cn.c[0]", "cn.c[1]", "cn.c", "cn")); + assertThat( + flattenedMap2.values(), + containsInAnyOrder(1, 2, 3, 4, Arrays.asList(3, 4), Collections.singletonMap("c", Arrays.asList(3, 4))) + ); + Map flattenedMap3 = JsonFlattener.flattenAsMap("{}"); + assertThat(flattenedMap3.keySet(), is(empty())); + assertThat(flattenedMap3.values(), is(empty())); + } + + @Test + public void testFlattenAsMapComplex() { + Map flattenedMap1 = JsonFlattener.flattenAsMap("{\n" + // + " \"a\": {\n" + // + " \"b\": 1,\n" + // + " \"c\": null,\n" + // + " \"d\": [\n" + // + " false,\n" + // + " true\n" + // + " ]\n" + // + " },\n" + // + " \"e\": \"f\",\n" + // + " \"g\": 2.30\n" + // + "}"); + assertThat(flattenedMap1.keySet(), containsInAnyOrder("a.b", "a.c", "a.d[0]", "a.d[1]", "a.d", "a", "e", "g")); + HashMap subMap1 = new HashMap<>(); + subMap1.put("b", 1); + subMap1.put("c", null); + subMap1.put("d", Arrays.asList(false, true)); + assertThat(flattenedMap1.values(), containsInAnyOrder(1, null, false, true, Arrays.asList(false, true), subMap1, "f", 2.3)); + Map flattenedMap2 = JsonFlattener.flattenAsMap( + "{\"a\":{\"b\":1,\"c\":null,\"d\":[false,{\"i\":{\"j\":[false,true,\"xy\"]}}]},\"e\":\"f\",\"g\":2.3,\"z\":[]}" + ); + assertThat( + flattenedMap2.keySet(), + containsInAnyOrder( + "a.b", + "a.c", + "a.d[0]", + "a.d[1].i.j[0]", + "a.d[1].i.j[1]", + "a.d[1].i.j[2]", + "a.d[1].i.j", + "a.d[1].i", + "a.d[1]", + "a.d", + "a", + "e", + "g", + "z" + ) + ); + subMap1 = new HashMap<>(); + subMap1.put("b", 1); + subMap1.put("c", null); + subMap1.put( + "d", + Arrays.asList(false, Collections.singletonMap("i", Collections.singletonMap("j", Arrays.asList(false, true, "xy")))) + ); + assertThat( + flattenedMap2.values(), + containsInAnyOrder( + 1, + null, + false, + false, + true, + "xy", + Arrays.asList(false, true, "xy"), + Collections.singletonMap("j", Arrays.asList(false, true, "xy")), + Collections.singletonMap("i", Collections.singletonMap("j", Arrays.asList(false, true, "xy"))), + Arrays.asList(false, Collections.singletonMap("i", Collections.singletonMap("j", Arrays.asList(false, true, "xy")))), + subMap1, + "f", + 2.3, + Collections.emptyList() + ) + ); + Map flattenedMap3 = JsonFlattener.flattenAsMap("{\n" + // + "\t\"glossary\": {\n" + // + "\t\t\"title\": \"example glossary\",\n" + // + "\t\t\"GlossDiv\": {\n" + // + "\t\t\t\"title\": \"S\",\n" + // + "\t\t\t\"GlossList\": {\n" + // + "\t\t\t\t\"GlossEntry\": {\n" + // + "\t\t\t\t\t\"ID\": \"SGML\",\n" + // + "\t\t\t\t\t\"SortAs\": \"SGML\",\n" + // + "\t\t\t\t\t\"GlossTerm\": \"Standard Generalized Markup Language\",\n" + // + "\t\t\t\t\t\"Acronym\": \"SGML\",\n" + // + "\t\t\t\t\t\"Abbrev\": \"ISO 8879:1986\",\n" + // + "\t\t\t\t\t\"GlossDef\": {\n" + // + "\t\t\t\t\t\t\"para\": \"A meta-markup language, used to create markup languages such as DocBook.\",\n" + // + "\t\t\t\t\t\t\"GlossSeeAlso\": [\n" + // + "\t\t\t\t\t\t\t\"GML\",\n" + // + "\t\t\t\t\t\t\t\"XML\"\n" + // + "\t\t\t\t\t\t]\n" + // + "\t\t\t\t\t},\n" + // + "\t\t\t\t\t\"GlossSee\": \"markup\"\n" + // + "\t\t\t\t}\n" + // + "\t\t\t}\n" + // + "\t\t}\n" + // + "\t}\n" + // + "}"); + assertThat( + flattenedMap3.keySet(), + containsInAnyOrder( + "glossary.title", + "glossary.GlossDiv.title", + "glossary.GlossDiv.GlossList.GlossEntry.ID", + "glossary.GlossDiv.GlossList.GlossEntry.SortAs", + "glossary.GlossDiv.GlossList.GlossEntry.GlossTerm", + "glossary.GlossDiv.GlossList.GlossEntry.Acronym", + "glossary.GlossDiv.GlossList.GlossEntry.Abbrev", + "glossary.GlossDiv.GlossList.GlossEntry.GlossDef.para", + "glossary.GlossDiv.GlossList.GlossEntry.GlossDef.GlossSeeAlso[0]", + "glossary.GlossDiv.GlossList.GlossEntry.GlossDef.GlossSeeAlso[1]", + "glossary.GlossDiv.GlossList.GlossEntry.GlossDef.GlossSeeAlso", + "glossary.GlossDiv.GlossList.GlossEntry.GlossDef", + "glossary.GlossDiv.GlossList.GlossEntry.GlossSee", + "glossary.GlossDiv.GlossList.GlossEntry", + "glossary.GlossDiv.GlossList", + "glossary.GlossDiv", + "glossary" + ) + ); + assertThat( + flattenedMap3.values(), + containsInAnyOrder( + "example glossary", + "S", + "SGML", + "SGML", + "Standard Generalized Markup Language", + "SGML", + "ISO 8879:1986", + "A meta-markup language, used to create markup languages such as DocBook.", + "GML", + "XML", + Arrays.asList("GML", "XML"), + Map.of( + "para", + "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso", + Arrays.asList("GML", "XML") + ), + "markup", + Map.of( + "ID", + "SGML", + "SortAs", + "SGML", + "GlossTerm", + "Standard Generalized Markup Language", + "Acronym", + "SGML", + "Abbrev", + "ISO 8879:1986", + "GlossDef", + Map.of( + "para", + "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso", + Arrays.asList("GML", "XML") + ), + "GlossSee", + "markup" + ), + Map.of( + "GlossEntry", + Map.of( + "ID", + "SGML", + "SortAs", + "SGML", + "GlossTerm", + "Standard Generalized Markup Language", + "Acronym", + "SGML", + "Abbrev", + "ISO 8879:1986", + "GlossDef", + Map.of( + "para", + "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso", + Arrays.asList("GML", "XML") + ), + "GlossSee", + "markup" + ) + ), + Map.of( + "title", + "S", + "GlossList", + Map.of( + "GlossEntry", + Map.of( + "ID", + "SGML", + "SortAs", + "SGML", + "GlossTerm", + "Standard Generalized Markup Language", + "Acronym", + "SGML", + "Abbrev", + "ISO 8879:1986", + "GlossDef", + Map.of( + "para", + "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso", + Arrays.asList("GML", "XML") + ), + "GlossSee", + "markup" + ) + ) + ), + Map.of( + "title", + "example glossary", + "GlossDiv", + Map.of( + "title", + "S", + "GlossList", + Map.of( + "GlossEntry", + Map.of( + "ID", + "SGML", + "SortAs", + "SGML", + "GlossTerm", + "Standard Generalized Markup Language", + "Acronym", + "SGML", + "Abbrev", + "ISO 8879:1986", + "GlossDef", + Map.of( + "para", + "A meta-markup language, used to create markup languages such as DocBook.", + "GlossSeeAlso", + Arrays.asList("GML", "XML") + ), + "GlossSee", + "markup" + ) + ) + ) + ) + ) + ); + Map flattenedMap4 = JsonFlattener.flattenAsMap("{\n" + // + "\t\"arrayOfObjects\": [\n" + // + "\t\ttrue,\n" + // + "\t\t{\n" + // + "\t\t\t\"x\": 1,\n" + // + "\t\t\t\"y\": 2,\n" + // + "\t\t\t\"z\": [\n" + // + "\t\t\t\t3,\n" + // + "\t\t\t\t4,\n" + // + "\t\t\t\t5\n" + // + "\t\t\t]\n" + // + "\t\t},\n" + // + "\t\t[\n" + // + "\t\t\t6,\n" + // + "\t\t\t7,\n" + // + "\t\t\t8\n" + // + "\t\t],\n" + // + "\t\t[\n" + // + "\t\t\t[\n" + // + "\t\t\t\t9,\n" + // + "\t\t\t\t10\n" + // + "\t\t\t],\n" + // + "\t\t\t11,\n" + // + "\t\t\t12\n" + // + "\t\t],\n" + // + "\t\tfalse\n" + // + "\t],\n" + // + "\t\"boolean\": true,\n" + // + "\t\"color\": \"#82b92c\",\n" + // + "\t\"null\": null,\n" + // + "\t\"number\": 123,\n" + // + "\t\"object\": {\n" + // + "\t\t\"a\": \"b\",\n" + // + "\t\t\"c\": \"d\",\n" + // + "\t\t\"e\": \"f\"\n" + // + "\t},\n" + // + "\t\"string\": \"Hello World\"\n" + // + "}"); + assertThat( + flattenedMap4.keySet(), + containsInAnyOrder( + "arrayOfObjects[0]", + "arrayOfObjects[1].x", + "arrayOfObjects[1].y", + "arrayOfObjects[1].z[0]", + "arrayOfObjects[1].z[1]", + "arrayOfObjects[1].z[2]", + "arrayOfObjects[1].z", + "arrayOfObjects[1]", + "arrayOfObjects[2][0]", + "arrayOfObjects[2][1]", + "arrayOfObjects[2][2]", + "arrayOfObjects[2]", + "arrayOfObjects[3][0][0]", + "arrayOfObjects[3][0][1]", + "arrayOfObjects[3][0]", + "arrayOfObjects[3][1]", + "arrayOfObjects[3][2]", + "arrayOfObjects[3]", + "arrayOfObjects[4]", + "arrayOfObjects", + "boolean", + "color", + "null", + "number", + "object.a", + "object.c", + "object.e", + "object", + "string" + ) + ); + assertThat( + flattenedMap4.values(), + containsInAnyOrder( + true, + 1, + 2, + 3, + 4, + 5, + Arrays.asList(3, 4, 5), + Map.of("x", 1, "y", 2, "z", Arrays.asList(3, 4, 5)), + 6, + 7, + 8, + Arrays.asList(6, 7, 8), + 9, + 10, + Arrays.asList(9, 10), + 11, + 12, + Arrays.asList(Arrays.asList(9, 10), 11, 12), + false, + Arrays.asList( + true, + Map.of("x", 1, "y", 2, "z", Arrays.asList(3, 4, 5)), + Arrays.asList(6, 7, 8), + Arrays.asList(Arrays.asList(9, 10), 11, 12), + false + ), + true, + "#82b92c", + null, + 123, + "b", + "d", + "f", + Map.of("a", "b", "c", "d", "e", "f"), + "Hello World" + ) + ); + } +} diff --git a/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java b/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java new file mode 100644 index 0000000000..170f0a9853 --- /dev/null +++ b/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java @@ -0,0 +1,510 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ + +package org.opensearch.security.support; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Set; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.runner.RunWith; + +import org.opensearch.action.DocWriteRequest; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.bulk.BulkItemResponse; +import org.opensearch.action.bulk.BulkRequest; +import org.opensearch.action.bulk.BulkResponse; +import org.opensearch.action.get.GetResponse; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.get.MultiGetResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.support.ActiveShardCount; +import org.opensearch.client.AdminClient; +import org.opensearch.client.Client; +import org.opensearch.client.IndicesAdminClient; +import org.opensearch.common.CheckedSupplier; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesArray; +import org.opensearch.index.get.GetResult; +import org.opensearch.security.DefaultObjectMapper; +import org.opensearch.security.securityconf.impl.CType; +import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; +import org.opensearch.security.state.SecurityConfig; +import org.opensearch.threadpool.ThreadPool; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.opensearch.security.configuration.ConfigurationRepository.DEFAULT_CONFIG_VERSION; +import static org.opensearch.security.support.YamlConfigReader.emptyYamlConfigFor; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.isA; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@RunWith(MockitoJUnitRunner.class) +public class SecurityIndexHandlerTest { + + final static String INDEX_NAME = "some_index"; + + final static String CONFIG_YAML = "_meta: \n" + + " type: \"config\"\n" + + " config_version: 2\n" + + "config:\n" + + " dynamic:\n" + + " http:\n" + + " anonymous_auth_enabled: false\n"; + + final static String USERS_YAML = "_meta:\n" + + " type: \"internalusers\"\n" + + " config_version: 2\n" + + "admin:\n" + + " hash: \"$2y$12$erlkZeSv7eRMa1vs3UgDl.xoqu1P9GY94Toj1BwdvJiq7eKTOjQjS\"\n" + + " reserved: true\n" + + " backend_roles:\n" + + " - \"admin\"\n" + + " description: \"Some admin user\"\n"; + + final static String ROLES_YAML = "_meta:\n" + " type: \"roles\"\n" + " config_version: 2\n" + "some_role:\n" + " reserved: true\n"; + + final static String ROLES_MAPPING_YAML = "_meta:\n" + + " type: \"rolesmapping\"\n" + + " config_version: 2\n" + + "all_access: \n" + + " reserved: false\n"; + + static final Map> YAML = Map.of( + CType.ACTIONGROUPS, + () -> emptyYamlConfigFor(CType.ACTIONGROUPS), + CType.ALLOWLIST, + () -> emptyYamlConfigFor(CType.ALLOWLIST), + CType.AUDIT, + () -> emptyYamlConfigFor(CType.AUDIT), + CType.CONFIG, + () -> CONFIG_YAML, + CType.INTERNALUSERS, + () -> USERS_YAML, + CType.NODESDN, + () -> emptyYamlConfigFor(CType.NODESDN), + CType.ROLES, + () -> ROLES_YAML, + CType.ROLESMAPPING, + () -> ROLES_MAPPING_YAML, + CType.TENANTS, + () -> emptyYamlConfigFor(CType.TENANTS), + CType.WHITELIST, + () -> emptyYamlConfigFor(CType.WHITELIST) + ); + + @Rule + public TemporaryFolder temporaryFolder = new TemporaryFolder(); + + @Mock + private Client client; + + @Mock + private ThreadPool threadPool; + + @Mock + private IndicesAdminClient indicesAdminClient; + + private Path configFolder; + + private ThreadContext threadContext; + + private SecurityIndexHandler securityIndexHandler; + + @Before + public void setupClient() throws IOException { + when(client.admin()).thenReturn(mock(AdminClient.class)); + when(client.admin().indices()).thenReturn(indicesAdminClient); + when(client.threadPool()).thenReturn(threadPool); + threadContext = new ThreadContext(Settings.EMPTY); + when(client.threadPool()).thenReturn(threadPool); + when(threadPool.getThreadContext()).thenReturn(threadContext); + configFolder = temporaryFolder.newFolder("config").toPath(); + securityIndexHandler = new SecurityIndexHandler(INDEX_NAME, Settings.EMPTY, client); + } + + @Test + public void testCreateIndex_shouldCreateIndex() { + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + actionListener.onResponse(new CreateIndexResponse(true, true, "some_index")); + return null; + }).when(indicesAdminClient).create(any(), any()); + + securityIndexHandler.createIndex(ActionListener.wrap(Assert::assertTrue, Assert::assertNull)); + + final var requestCaptor = ArgumentCaptor.forClass(CreateIndexRequest.class); + + verify(indicesAdminClient).create(requestCaptor.capture(), any()); + + final var createRequest = requestCaptor.getValue(); + assertEquals(INDEX_NAME, createRequest.index()); + for (final var setting : SecurityIndexHandler.INDEX_SETTINGS.entrySet()) + assertEquals(setting.getValue().toString(), createRequest.settings().get(setting.getKey())); + + assertEquals(ActiveShardCount.ONE, createRequest.waitForActiveShards()); + } + + @Test + public void testCreateIndex_shouldReturnSecurityExceptionIfItCanNotCreateIndex() { + + final var listener = spy(ActionListener.wrap(r -> fail("Unexpected behave"), e -> { + assertEquals(SecurityException.class, e.getClass()); + assertEquals("Couldn't create security index " + INDEX_NAME, e.getMessage()); + })); + + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + actionListener.onResponse(new CreateIndexResponse(false, false, "some_index")); + return null; + }).when(indicesAdminClient).create(any(), any()); + + securityIndexHandler.createIndex(listener); + + verify(indicesAdminClient).create(isA(CreateIndexRequest.class), any()); + verify(listener).onFailure(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldFailIfRequiredConfigFilesAreMissing() { + final var listener = spy(ActionListener.>wrap(r -> fail("Unexpected behave"), e -> { + assertEquals(SecurityException.class, e.getClass()); + assertThat(e.getMessage(), containsString("Couldn't find configuration file")); + })); + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldFailIfBulkHasFailures() throws IOException { + final var failedBulkResponse = new BulkResponse( + new BulkItemResponse[] { + new BulkItemResponse(1, DocWriteRequest.OpType.CREATE, new BulkItemResponse.Failure("a", "b", new Exception())) }, + 100L + ); + final var listener = spy(ActionListener.>wrap(r -> fail("Unexpected behave"), e -> { + assertEquals(SecurityException.class, e.getClass()); + assertEquals(e.getMessage(), failedBulkResponse.buildFailureMessage()); + })); + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + actionListener.onResponse(failedBulkResponse); + return null; + }).when(client).bulk(any(BulkRequest.class), any()); + for (final var c : CType.REQUIRED_CONFIG_FILES) { + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + io.write(YAML.get(c).get()); + io.flush(); + } + } + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + verify(listener).onFailure(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldCreateSetOfSecurityConfigs() throws IOException { + + final var listener = spy(ActionListener.>wrap(configuration -> { + for (final var sc : configuration) { + assertTrue(sc.lastModified().isEmpty()); + assertNotNull(sc.hash()); + } + }, e -> fail("Unexpected behave"))); + + for (final var c : CType.REQUIRED_CONFIG_FILES) { + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + final var source = YAML.get(c).get(); + io.write(source); + io.flush(); + } + } + + final var bulkRequestCaptor = ArgumentCaptor.forClass(BulkRequest.class); + + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(BulkResponse.class); + when(r.hasFailures()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).bulk(bulkRequestCaptor.capture(), any()); + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + + final var bulkRequest = bulkRequestCaptor.getValue(); + for (final var r : bulkRequest.requests()) { + final var indexRequest = (IndexRequest) r; + assertEquals(INDEX_NAME, r.index()); + assertEquals(DocWriteRequest.OpType.INDEX, indexRequest.opType()); + } + verify(listener).onResponse(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldSkipAudit() throws IOException { + final var listener = spy( + ActionListener.>wrap( + configuration -> assertFalse(configuration.stream().anyMatch(sc -> sc.type() == CType.AUDIT)), + e -> fail("Unexpected behave") + ) + ); + + for (final var c : CType.REQUIRED_CONFIG_FILES) { + if (c == CType.AUDIT) continue; + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + final var source = YAML.get(c).get(); + io.write(source); + io.flush(); + } + } + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(BulkResponse.class); + when(r.hasFailures()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).bulk(any(BulkRequest.class), any()); + + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + verify(listener).onResponse(any()); + } + + @Test + public void testUploadDefaultConfiguration_shouldSkipWhitelist() throws IOException { + final var listener = spy( + ActionListener.>wrap( + configuration -> assertFalse(configuration.stream().anyMatch(sc -> sc.type() == CType.WHITELIST)), + e -> fail("Unexpected behave") + ) + ); + + for (final var c : CType.REQUIRED_CONFIG_FILES) { + if (c == CType.WHITELIST) continue; + try (final var io = Files.newBufferedWriter(c.configFile(configFolder))) { + final var source = YAML.get(c).get(); + io.write(source); + io.flush(); + } + } + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(BulkResponse.class); + when(r.hasFailures()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).bulk(any(BulkRequest.class), any()); + + securityIndexHandler.uploadDefaultConfiguration(configFolder, listener); + verify(listener).onResponse(any()); + } + + @Test + public void testLoadConfiguration_shouldFailIfResponseHasFailures() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals(SecurityException.class, e.getClass()) + ) + ); + + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var r = mock(MultiGetResponse.class); + final var mr = mock(MultiGetItemResponse.class); + when(mr.isFailed()).thenReturn(true); + when(mr.getFailure()).thenReturn(new MultiGetResponse.Failure("a", "id", new Exception())); + when(r.getResponses()).thenReturn(new MultiGetItemResponse[] { mr }); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + + securityIndexHandler.loadConfiguration(configuration(), listener); + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldFailIfNoRequiredConfigInResponse() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals("Missing required configuration for type: CONFIG", e.getMessage()) + ) + ); + doAnswer(invocation -> { + ActionListener actionListener = invocation.getArgument(1); + final var getResult = mock(GetResult.class); + final var r = new MultiGetResponse(new MultiGetItemResponse[] { new MultiGetItemResponse(new GetResponse(getResult), null) }); + when(getResult.getId()).thenReturn(CType.CONFIG.toLCString()); + when(getResult.isExists()).thenReturn(false); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldFailForUnsupportedVersion() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals("Version 1 is not supported for CONFIG", e.getMessage()) + ) + ); + doAnswer(invocation -> { + + final var objectMapper = DefaultObjectMapper.objectMapper; + + ActionListener actionListener = invocation.getArgument(1); + final var getResult = mock(GetResult.class); + final var r = new MultiGetResponse(new MultiGetItemResponse[] { new MultiGetItemResponse(new GetResponse(getResult), null) }); + when(getResult.getId()).thenReturn(CType.CONFIG.toLCString()); + when(getResult.isExists()).thenReturn(true); + + final var oldVersionJson = objectMapper.createObjectNode() + .set("opendistro_security", objectMapper.createObjectNode().set("dynamic", objectMapper.createObjectNode())) + .toString() + .getBytes(StandardCharsets.UTF_8); + final var configResponse = objectMapper.createObjectNode().put(CType.CONFIG.toLCString(), oldVersionJson); + final var source = objectMapper.writeValueAsBytes(configResponse); + when(getResult.sourceRef()).thenReturn(new BytesArray(source, 0, source.length)); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldFailForUnparseableConfig() { + final var listener = spy( + ActionListener.>>wrap( + r -> fail("Unexpected behave"), + e -> assertEquals("Couldn't parse content for CONFIG", e.getMessage()) + ) + ); + doAnswer(invocation -> { + + final var objectMapper = DefaultObjectMapper.objectMapper; + + ActionListener actionListener = invocation.getArgument(1); + final var getResult = mock(GetResult.class); + final var r = new MultiGetResponse(new MultiGetItemResponse[] { new MultiGetItemResponse(new GetResponse(getResult), null) }); + when(getResult.getId()).thenReturn(CType.CONFIG.toLCString()); + when(getResult.isExists()).thenReturn(true); + + final var configResponse = objectMapper.createObjectNode() + .put( + CType.CONFIG.toLCString(), + objectMapper.createObjectNode() + .set("_meta", objectMapper.createObjectNode().put("type", CType.CONFIG.toLCString())) + .toString() + .getBytes(StandardCharsets.UTF_8) + ); + final var source = objectMapper.writeValueAsBytes(configResponse); + when(getResult.sourceRef()).thenReturn(new BytesArray(source, 0, source.length)); + actionListener.onResponse(r); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onFailure(any()); + } + + @Test + public void testLoadConfiguration_shouldBuildSecurityConfig() { + final var listener = spy(ActionListener.>>wrap(config -> { + assertEquals(CType.values().length, config.keySet().size()); + for (final var c : CType.values()) { + assertTrue(c.toLCString(), config.containsKey(c)); + } + }, e -> fail("Unexpected behave"))); + doAnswer(invocation -> { + final var objectMapper = DefaultObjectMapper.objectMapper; + ActionListener actionListener = invocation.getArgument(1); + + final var responses = new MultiGetItemResponse[CType.values().length]; + var counter = 0; + for (final var c : CType.values()) { + final var getResult = mock(GetResult.class); + if (!c.emptyIfMissing()) { + when(getResult.getId()).thenReturn(c.toLCString()); + when(getResult.isExists()).thenReturn(true); + + final var minimumRequiredConfig = minimumRequiredConfig(c); + if (c == CType.CONFIG) minimumRequiredConfig.set( + "config", + objectMapper.createObjectNode().set("dynamic", objectMapper.createObjectNode()) + ); + + final var source = objectMapper.writeValueAsBytes( + objectMapper.createObjectNode() + .put(c.toLCString(), minimumRequiredConfig.toString().getBytes(StandardCharsets.UTF_8)) + ); + + when(getResult.sourceRef()).thenReturn(new BytesArray(source, 0, source.length)); + + responses[counter] = new MultiGetItemResponse(new GetResponse(getResult), null); + } else { + when(getResult.getId()).thenReturn(c.toLCString()); + when(getResult.isExists()).thenReturn(false); + responses[counter] = new MultiGetItemResponse(new GetResponse(getResult), null); + } + counter++; + } + actionListener.onResponse(new MultiGetResponse(responses)); + return null; + }).when(client).multiGet(any(MultiGetRequest.class), any()); + securityIndexHandler.loadConfiguration(configuration(), listener); + + verify(listener).onResponse(any()); + } + + private ObjectNode minimumRequiredConfig(final CType cType) { + final var objectMapper = DefaultObjectMapper.objectMapper; + return objectMapper.createObjectNode() + .set("_meta", objectMapper.createObjectNode().put("type", cType.toLCString()).put("config_version", DEFAULT_CONFIG_VERSION)); + } + + private Set configuration() { + return Set.of(new SecurityConfig(CType.CONFIG, "aaa", null), new SecurityConfig(CType.AUDIT, "bbb", null)); + } + +} diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java index 9415634596..e14574873e 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexDisabledTests.java @@ -14,7 +14,7 @@ import java.io.IOException; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java index 25514c4118..37b4f1bc0f 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionDisabledTests.java @@ -14,7 +14,7 @@ import java.io.IOException; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.Test; diff --git a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java index 766db1eca8..7db072761f 100644 --- a/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java +++ b/src/test/java/org/opensearch/security/system_indices/SystemIndexPermissionEnabledTests.java @@ -12,7 +12,7 @@ package org.opensearch.security.system_indices; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.http.HttpStatus; import org.junit.Before; import org.junit.Test; @@ -79,7 +79,8 @@ public void testSearchAsNormalUser() throws Exception { if (index.equals(ACCESSIBLE_ONLY_BY_SUPER_ADMIN) || index.equals(SYSTEM_INDEX_WITH_NO_ASSOCIATED_ROLE_PERMISSIONS)) { validateForbiddenResponse(response, "", normalUser); } else { - validateSearchResponse(response, 0); + // got 1 hits because system index permissions are enabled + validateSearchResponse(response, 1); } } diff --git a/src/test/java/org/opensearch/security/test/SingleClusterTest.java b/src/test/java/org/opensearch/security/test/SingleClusterTest.java index 2839e1e283..cdde57a5c0 100644 --- a/src/test/java/org/opensearch/security/test/SingleClusterTest.java +++ b/src/test/java/org/opensearch/security/test/SingleClusterTest.java @@ -83,18 +83,6 @@ protected void setup( setup(initTransportClientSettings, dynamicSecuritySettings, nodeOverride, initSecurityIndex, ClusterConfiguration.DEFAULT); } - protected void restart( - Settings initTransportClientSettings, - DynamicSecurityConfig dynamicSecuritySettings, - Settings nodeOverride, - boolean initOpendistroSecurityIndex - ) throws Exception { - clusterInfo = clusterHelper.startCluster(minimumSecuritySettings(ccs(nodeOverride)), ClusterConfiguration.DEFAULT); - if (initOpendistroSecurityIndex && dynamicSecuritySettings != null) { - initialize(clusterHelper, clusterInfo, dynamicSecuritySettings); - } - } - private Settings ccs(Settings nodeOverride) throws Exception { if (remoteClusterHelper != null) { Assert.assertNull("No remote clusters", remoteClusterInfo); diff --git a/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java b/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java index 43e7afc559..1710a93875 100644 --- a/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java +++ b/src/test/java/org/opensearch/security/test/helper/rest/RestHelper.java @@ -90,6 +90,10 @@ import org.opensearch.security.test.helper.cluster.ClusterInfo; import org.opensearch.security.test.helper.file.FileHelper; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; + public class RestHelper { protected final Logger log = LogManager.getLogger(RestHelper.class); @@ -393,7 +397,7 @@ public static class HttpResponse { public HttpResponse(SimpleHttpResponse inner) throws IllegalStateException, IOException { super(); this.inner = inner; - if (inner.getBody() == null) { // head request does not have a entity + if (inner.getBody() == null) { // head request does not have an entity this.body = ""; } else { this.body = inner.getBodyText(); @@ -402,6 +406,29 @@ public HttpResponse(SimpleHttpResponse inner) throws IllegalStateException, IOEx this.statusCode = inner.getCode(); this.statusReason = inner.getReasonPhrase(); this.protocolVersion = inner.getVersion(); + + if (this.body.length() != 0) { + verifyBodyContentType(); + } + } + + private void verifyBodyContentType() { + final String contentType = this.getHeaders() + .stream() + .filter(h -> HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(h.getName())) + .map(Header::getValue) + .findFirst() + .orElseThrow(() -> new RuntimeException("No content type found. Headers:\n" + getHeaders() + "\n\nBody:\n" + body)); + + if (contentType.contains("application/json")) { + assertThat("Response body format was not json, body: " + body, body.charAt(0), equalTo('{')); + } else { + assertThat( + "Response body format was json, whereas content-type was " + contentType + ", body: " + body, + body.charAt(0), + not(equalTo('{')) + ); + } } public String getContentType() { diff --git a/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java b/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java new file mode 100644 index 0000000000..3b43311679 --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/democonfig/CertificateGeneratorTests.java @@ -0,0 +1,172 @@ +/* + * 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.tools.democonfig; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileReader; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.Instant; +import java.time.LocalDate; +import java.time.Period; +import java.util.Base64; +import java.util.Date; +import java.util.TimeZone; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.security.tools.democonfig.util.NoExitSecurityManager; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.is; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createDirectory; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.deleteDirectoryRecursive; +import static org.junit.Assert.fail; + +public class CertificateGeneratorTests { + + private static Installer installer; + + @Before + public void setUp() { + installer = Installer.getInstance(); + installer.buildOptions(); + installer.OPENSEARCH_CONF_DIR = System.getProperty("user.dir") + File.separator + "test-conf"; + createDirectory(installer.OPENSEARCH_CONF_DIR); + } + + @After + public void tearDown() { + deleteDirectoryRecursive(installer.OPENSEARCH_CONF_DIR); + Installer.resetInstance(); + } + + @Test + public void testCreateDemoCertificates() throws Exception { + CertificateGenerator certificateGenerator = new CertificateGenerator(installer); + Certificates[] certificatesArray = Certificates.values(); + + certificateGenerator.createDemoCertificates(); + + // root-ca.pem, esnode.pem, esnode-key.pem, kirk.pem, kirk-key.pem + int expectedNumberOfCertificateFiles = 5; + + int certsFound = 0; + + for (Certificates cert : certificatesArray) { + String certFilePath = installer.OPENSEARCH_CONF_DIR + File.separator + cert.getFileName(); + File certFile = new File(certFilePath); + assertThat(certFile.exists(), is(equalTo(true))); + assertThat(certFile.canRead(), is(equalTo(true))); + + if (certFilePath.endsWith("-key.pem")) { + checkPrivateKeyValidity(certFilePath); + } else { + checkCertificateValidity(certFilePath); + } + + // increment a count since a valid certificate was found + certsFound++; + } + + assertThat(certsFound, equalTo(expectedNumberOfCertificateFiles)); + } + + @Test + public void testCreateDemoCertificates_invalidPath() { + installer.OPENSEARCH_CONF_DIR = "invalidPath"; + CertificateGenerator certificateGenerator = new CertificateGenerator(installer); + try { + System.setSecurityManager(new NoExitSecurityManager()); + certificateGenerator.createDemoCertificates(); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + } + + private static void checkCertificateValidity(String certPath) throws Exception { + try (FileInputStream certInputStream = new FileInputStream(certPath)) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + Certificate certificate = cf.generateCertificate(certInputStream); + + if (certificate instanceof X509Certificate) { + X509Certificate x509Certificate = (X509Certificate) certificate; + Date expiryDate = x509Certificate.getNotAfter(); + Instant expiry = expiryDate.toInstant(); + + Period duration = getPeriodBetween(x509Certificate.getNotBefore().toInstant(), expiry); + + // we check that cert is valid for total of ~10 yrs + // we don't check days as leaps years may cause flaky-ness + assertThat(duration.getYears(), equalTo(9)); + assertThat(duration.getMonths(), equalTo(11)); + + x509Certificate.checkValidity(); + verifyExpiryAtLeastAYearFromNow(expiry); + + assertThat(x509Certificate.getSigAlgName(), is(equalTo("SHA256withRSA"))); + } + } + } + + private static void verifyExpiryAtLeastAYearFromNow(Instant expiry) { + Period gap = getPeriodBetween(Instant.now(), expiry); + assertThat(gap.getYears(), greaterThanOrEqualTo(1)); + } + + private static Period getPeriodBetween(Instant start, Instant end) { + LocalDate startDate = LocalDate.ofInstant(start, TimeZone.getTimeZone("EDT").toZoneId()); + LocalDate endDate = LocalDate.ofInstant(end, TimeZone.getTimeZone("EDT").toZoneId()); + + return Period.between(startDate, endDate); + } + + private void checkPrivateKeyValidity(String keyPath) { + try { + String pemContent = readPEMFile(keyPath); + + String base64Data = pemContent.replaceAll("-----BEGIN PRIVATE KEY-----|-----END PRIVATE KEY-----", "").replaceAll("\\s", ""); + + byte[] keyBytes = Base64.getDecoder().decode(base64Data); + KeyFactory kf = KeyFactory.getInstance("RSA"); + PrivateKey key = kf.generatePrivate(new PKCS8EncodedKeySpec(keyBytes)); + assertThat(key.getFormat(), is(equalTo("PKCS#8"))); + assertThat(key.getAlgorithm(), is(equalTo("RSA"))); + assertThat(key.isDestroyed(), is(equalTo(false))); + } catch (Exception e) { + fail("Error checking key validity: " + e.getMessage()); + } + } + + private static String readPEMFile(String pemFilePath) throws Exception { + StringBuilder pemContent = new StringBuilder(); + try (BufferedReader reader = new BufferedReader(new FileReader(pemFilePath))) { + String line; + while ((line = reader.readLine()) != null) { + pemContent.append(line).append("\n"); + } + } + return pemContent.toString(); + } +} diff --git a/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java b/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java new file mode 100644 index 0000000000..268bd9ea0e --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/democonfig/InstallerTests.java @@ -0,0 +1,515 @@ +/* + * 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.tools.democonfig; + +// CS-SUPPRESS-SINGLE: RegexpSingleline extension key-word is used in file ext variable +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.HashSet; +import java.util.Set; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.security.tools.democonfig.util.NoExitSecurityManager; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.opensearch.security.tools.democonfig.Installer.RPM_DEB_OPENSEARCH_HOME; +import static org.opensearch.security.tools.democonfig.Installer.printScriptHeaders; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createDirectory; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createFile; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.deleteDirectoryRecursive; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.fail; + +public class InstallerTests { + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final InputStream originalIn = System.in; + + private static Installer installer; + + @Before + public void setUpStreams() { + System.setOut(new PrintStream(outContent)); + installer = Installer.getInstance(); + installer.buildOptions(); + } + + @After + public void restoreStreams() { + System.setOut(originalOut); + System.setIn(originalIn); + Installer.resetInstance(); + } + + @Test + public void testPrintScriptHeaders() { + printScriptHeaders(); + + String expectedOutput = "### OpenSearch Security Demo Installer" + + System.lineSeparator() + + "### ** Warning: Do not use on production or public reachable systems **" + + System.lineSeparator(); + assertThat(outContent.toString(), equalTo(expectedOutput)); + } + + @Test + public void testReadOptions_withoutHelpOption() { + // All options except Help `-h` + String[] validOptions = { "/scriptDir", "-y", "-i", "-c", "-s", "-t" }; + installer.readOptions(validOptions); + + assertThat(installer.SCRIPT_DIR, equalTo("/scriptDir")); + assertThat(installer.assumeyes, is(true)); + assertThat(installer.initsecurity, is(true)); + assertThat(installer.cluster_mode, is(true)); + assertThat(installer.skip_updates, equalTo(0)); + assertThat(installer.environment, equalTo(ExecutionEnvironment.TEST)); + } + + @Test + public void testReadOptions_help() { + try { + System.setSecurityManager(new NoExitSecurityManager()); + String[] helpOption = { "/scriptDir", "-h" }; + installer.readOptions(helpOption); + } catch (SecurityException e) { + // if help text printed correctly then exit code 0 is expected + assertThat(e.getMessage(), equalTo("System.exit(0) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString("usage: install_demo_configuration" + installer.FILE_EXTENSION + " [-c] [-h] [-i] [-s] [-t] [-y]"); + } + + @Test + public void testGatherUserInputs_withoutAssumeYes() { + // -i & -c option is not passed + String[] validOptions = { "/scriptDir" }; + installer.readOptions(validOptions); + assertThat(installer.assumeyes, is(false)); + assertThat(installer.initsecurity, is(false)); + assertThat(installer.cluster_mode, is(false)); + + // set initsecurity and cluster_mode to no + readInputStream("y" + System.lineSeparator() + "n" + System.lineSeparator() + "n" + System.lineSeparator()); // pass all 3 inputs as + // y + installer.gatherUserInputs(); + + verifyStdOutContainsString("Install demo certificates?"); + verifyStdOutContainsString("Initialize Security Modules?"); + verifyStdOutContainsString("Cluster mode requires additional setup of:"); + verifyStdOutContainsString(" - Virtual memory (vm.max_map_count)" + System.lineSeparator()); + verifyStdOutContainsString("Enable cluster mode?"); + + assertThat(installer.initsecurity, is(false)); + assertThat(installer.cluster_mode, is(false)); + + outContent.reset(); + + // set initsecurity and cluster_mode to no + readInputStream("y" + System.lineSeparator() + "y" + System.lineSeparator() + "y" + System.lineSeparator()); // pass all 3 inputs as + // y + installer.gatherUserInputs(); + + verifyStdOutContainsString("Install demo certificates?"); + verifyStdOutContainsString("Initialize Security Modules?"); + verifyStdOutContainsString("Cluster mode requires additional setup of:"); + verifyStdOutContainsString(" - Virtual memory (vm.max_map_count)" + System.lineSeparator()); + verifyStdOutContainsString("Enable cluster mode?"); + + assertThat(installer.initsecurity, is(true)); + assertThat(installer.cluster_mode, is(true)); + + outContent.reset(); + + // no to demo certificates + try { + System.setSecurityManager(new NoExitSecurityManager()); + + readInputStream("n" + System.lineSeparator() + "n" + System.lineSeparator() + "n" + System.lineSeparator()); + installer.gatherUserInputs(); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(0) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + verifyStdOutContainsString("Install demo certificates?"); + verifyStdOutDoesNotContainString("Initialize Security Modules?"); + verifyStdOutDoesNotContainString("Cluster mode requires additional setup of:"); + verifyStdOutDoesNotContainString(" - Virtual memory (vm.max_map_count)" + System.lineSeparator()); + verifyStdOutDoesNotContainString("Enable cluster mode?"); + + outContent.reset(); + + // pass initsecurity and cluster_mode options + String[] validOptionsIC = { "/scriptDir", "-i", "-c" }; + installer.readOptions(validOptionsIC); + assertThat(installer.assumeyes, is(false)); + assertThat(installer.initsecurity, is(true)); + assertThat(installer.cluster_mode, is(true)); + + readInputStream("y" + System.lineSeparator() + "y" + System.lineSeparator() + "y" + System.lineSeparator()); // pass all 3 inputs as + // y + installer.gatherUserInputs(); + + verifyStdOutContainsString("Install demo certificates?"); + verifyStdOutDoesNotContainString("Initialize Security Modules?"); + verifyStdOutDoesNotContainString("Enable cluster mode?"); + + assertThat(installer.initsecurity, is(true)); + assertThat(installer.cluster_mode, is(true)); + } + + @Test + public void testGatherInputs_withAssumeYes() { + String[] validOptionsYes = { "/scriptDir", "-y" }; + installer.readOptions(validOptionsYes); + assertThat(installer.assumeyes, is(true)); + + installer.gatherUserInputs(); + + assertThat(installer.initsecurity, is(false)); + assertThat(installer.cluster_mode, is(false)); + } + + @Test + public void testInitializeVariables_setBaseDir_invalidPath() { + String[] invalidScriptDirPath = { "/scriptDir", "-y" }; + installer.readOptions(invalidScriptDirPath); + + assertThrows("Expected NullPointerException to be thrown", NullPointerException.class, installer::initializeVariables); + + String[] invalidScriptDirPath2 = { "/opensearch/plugins/opensearch-security/tools", "-y" }; + installer.readOptions(invalidScriptDirPath2); + + try { + System.setSecurityManager(new NoExitSecurityManager()); + installer.initializeVariables(); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString("DEBUG: basedir does not exist"); + } + + @Test + public void testSetBaseDir_valid() { + String currentDir = System.getProperty("user.dir"); + + String[] validBaseDir = { currentDir, "-y" }; + installer.readOptions(validBaseDir); + + installer.setBaseDir(); + + String expectedBaseDirValue = new File(currentDir).getParentFile().getParentFile().getParentFile().getAbsolutePath() + + File.separator; + assertThat(installer.BASE_DIR, equalTo(expectedBaseDirValue)); + } + + @Test + public void testSetOpenSearchVariables_invalidPath() { + String currentDir = System.getProperty("user.dir"); + + String[] validBaseDir = { currentDir, "-y" }; + installer.readOptions(validBaseDir); + + try { + System.setSecurityManager(new NoExitSecurityManager()); + installer.setBaseDir(); + installer.setOpenSearchVariables(); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + + } + verifyStdOutContainsString("Unable to determine OpenSearch config file. Quit."); + verifyStdOutContainsString("Unable to determine OpenSearch bin directory. Quit."); + verifyStdOutContainsString("Unable to determine OpenSearch plugins directory. Quit."); + verifyStdOutContainsString("Unable to determine OpenSearch lib directory. Quit."); + + String expectedBaseDirValue = new File(currentDir).getParentFile().getParentFile().getParentFile().getAbsolutePath() + + File.separator; + String expectedOpensearchConfFilePath = expectedBaseDirValue + "config" + File.separator + "opensearch.yml"; + String expectedOpensearchBinDirPath = expectedBaseDirValue + "bin" + File.separator; + String expectedOpensearchPluginDirPath = expectedBaseDirValue + "plugins" + File.separator; + String expectedOpensearchLibDirPath = expectedBaseDirValue + "lib" + File.separator; + String expectedOpensearchInstallType = installer.determineInstallType(); + + assertThat(installer.OPENSEARCH_CONF_FILE, equalTo(expectedOpensearchConfFilePath)); + assertThat(installer.OPENSEARCH_BIN_DIR, equalTo(expectedOpensearchBinDirPath)); + assertThat(installer.OPENSEARCH_PLUGINS_DIR, equalTo(expectedOpensearchPluginDirPath)); + assertThat(installer.OPENSEARCH_LIB_PATH, equalTo(expectedOpensearchLibDirPath)); + assertThat(installer.OPENSEARCH_INSTALL_TYPE, equalTo(expectedOpensearchInstallType)); + + } + + @Test + public void testDetermineInstallType_windows() { + installer.OS = "Windows"; + + String installType = installer.determineInstallType(); + + assertThat(installType, equalTo(".zip")); + } + + @Test + public void testDetermineInstallType_rpm_deb() { + installer.OS = "Linux"; + String dir = System.getProperty("user.dir"); + installer.BASE_DIR = dir; + RPM_DEB_OPENSEARCH_HOME = new File(dir); + + String installType = installer.determineInstallType(); + + assertThat(installType, equalTo("rpm/deb")); + } + + @Test + public void testDetermineInstallType_default() { + installer.OS = "Anything else"; + installer.BASE_DIR = "/random-dir"; + String installType = installer.determineInstallType(); + + assertThat(installType, equalTo(".tar.gz")); + } + + @Test + public void testSetSecurityVariables() { + setUpSecurityDirectories(); + installer.setSecurityVariables(); + + assertThat(installer.OPENSEARCH_VERSION, is(equalTo("osVersion"))); + assertThat(installer.SECURITY_VERSION, is(equalTo("version"))); + tearDownSecurityDirectories(); + } + + @Test + public void testSetSecurityVariables_noSecurityPlugin() { + try { + System.setSecurityManager(new NoExitSecurityManager()); + + installer.setSecurityVariables(); + fail("Expected System.exit(-1) to be called"); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + } + + @Test + public void testPrintVariables() { + installer.OPENSEARCH_INSTALL_TYPE = "installType"; + installer.OS = "OS"; + installer.OPENSEARCH_CONF_DIR = "confDir"; + installer.OPENSEARCH_CONF_FILE = "confFile"; + installer.OPENSEARCH_BIN_DIR = "/bin"; + installer.OPENSEARCH_PLUGINS_DIR = "/plugins"; + installer.OPENSEARCH_LIB_PATH = "/lib"; + installer.OPENSEARCH_VERSION = "osVersion"; + installer.SECURITY_VERSION = "version"; + + installer.printVariables(); + + String expectedOutput = "OpenSearch install type: installType on OS" + + System.lineSeparator() + + "OpenSearch config dir: confDir" + + System.lineSeparator() + + "OpenSearch config file: confFile" + + System.lineSeparator() + + "OpenSearch bin dir: /bin" + + System.lineSeparator() + + "OpenSearch plugins dir: /plugins" + + System.lineSeparator() + + "OpenSearch lib dir: /lib" + + System.lineSeparator() + + "Detected OpenSearch Version: osVersion" + + System.lineSeparator() + + "Detected OpenSearch Security Version: version" + + System.lineSeparator(); + + assertThat(outContent.toString(), equalTo(expectedOutput)); + } + + @Test + public void testFinishScriptExecution() { + setUpSecurityDirectories(); + SecuritySettingsConfigurer.ADMIN_PASSWORD = "ble"; + + installer.finishScriptExecution(); + + String securityAdminScriptPath = installer.OPENSEARCH_PLUGINS_DIR + + "opensearch-security" + + File.separator + + "tools" + + File.separator + + "securityadmin" + + installer.FILE_EXTENSION; + String securityAdminDemoScriptPath = installer.OPENSEARCH_CONF_DIR + "securityadmin_demo" + installer.FILE_EXTENSION; + setWritePermissions(securityAdminDemoScriptPath); + + SecuritySettingsConfigurer securitySettingsConfigurer = new SecuritySettingsConfigurer(installer); + String lastLine = securitySettingsConfigurer.getSecurityAdminCommands(securityAdminScriptPath)[1]; + + String expectedOutput = "### Success" + + System.lineSeparator() + + "### Execute this script now on all your nodes and then start all nodes" + + System.lineSeparator() + + "### After the whole cluster is up execute: " + + System.lineSeparator() + + lastLine + + System.lineSeparator() + + "### or run ." + + File.separator + + "securityadmin_demo" + + installer.FILE_EXTENSION + + System.lineSeparator() + + "### After that you can also use the Security Plugin ConfigurationGUI" + + System.lineSeparator() + + "### To access your secured cluster open https://: and log in with admin/." + + System.lineSeparator() + + "### (Ignore the SSL certificate warning because we installed self-signed demo certificates)" + + System.lineSeparator(); + + assertThat(outContent.toString(), equalTo(expectedOutput)); + + tearDownSecurityDirectories(); + } + + @Test + public void testFinishScriptExecution_withInitSecurityEnabled() { + setUpSecurityDirectories(); + installer.initsecurity = true; + SecuritySettingsConfigurer.ADMIN_PASSWORD = "ble"; + + installer.finishScriptExecution(); + + String securityAdminScriptPath = installer.OPENSEARCH_PLUGINS_DIR + + "opensearch-security" + + File.separator + + "tools" + + File.separator + + "securityadmin" + + installer.FILE_EXTENSION; + String securityAdminDemoScriptPath = installer.OPENSEARCH_CONF_DIR + "securityadmin_demo" + installer.FILE_EXTENSION; + setWritePermissions(securityAdminDemoScriptPath); + + SecuritySettingsConfigurer securitySettingsConfigurer = new SecuritySettingsConfigurer(installer); + String lastLine = securitySettingsConfigurer.getSecurityAdminCommands(securityAdminScriptPath)[1]; + + String expectedOutput = "### Success" + + System.lineSeparator() + + "### Execute this script now on all your nodes and then start all nodes" + + System.lineSeparator() + + "### OpenSearch Security will be automatically initialized." + + System.lineSeparator() + + "### If you like to change the runtime configuration " + + System.lineSeparator() + + "### change the files in .." + + File.separator + + ".." + + File.separator + + ".." + + File.separator + + "config" + + File.separator + + "opensearch-security and execute: " + + System.lineSeparator() + + lastLine + + System.lineSeparator() + + "### or run ." + + File.separator + + "securityadmin_demo" + + installer.FILE_EXTENSION + + System.lineSeparator() + + "### To use the Security Plugin ConfigurationGUI" + + System.lineSeparator() + + "### To access your secured cluster open https://: and log in with admin/." + + System.lineSeparator() + + "### (Ignore the SSL certificate warning because we installed self-signed demo certificates)" + + System.lineSeparator(); + + assertThat(outContent.toString(), equalTo(expectedOutput)); + + tearDownSecurityDirectories(); + } + + private void readInputStream(String input) { + System.setIn(new ByteArrayInputStream(input.getBytes())); + } + + public void setUpSecurityDirectories() { + String currentDir = System.getProperty("user.dir"); + + String[] validBaseDir = { currentDir, "-y" }; + installer.readOptions(validBaseDir); + installer.setBaseDir(); + installer.OPENSEARCH_PLUGINS_DIR = installer.BASE_DIR + "plugins" + File.separator; + installer.OPENSEARCH_LIB_PATH = installer.BASE_DIR + "lib" + File.separator; + installer.OPENSEARCH_CONF_DIR = installer.BASE_DIR + "test-conf" + File.separator; + + createDirectory(installer.OPENSEARCH_PLUGINS_DIR); + createDirectory(installer.OPENSEARCH_LIB_PATH); + createDirectory(installer.OPENSEARCH_CONF_DIR); + createDirectory(installer.OPENSEARCH_PLUGINS_DIR + "opensearch-security"); + createFile(installer.OPENSEARCH_LIB_PATH + "opensearch-core-osVersion.jar"); + createFile(installer.OPENSEARCH_PLUGINS_DIR + "opensearch-security" + File.separator + "opensearch-security-version.jar"); + createFile(installer.OPENSEARCH_CONF_DIR + File.separator + "securityadmin_demo.sh"); + } + + public void tearDownSecurityDirectories() { + // Clean up testing directories or files + deleteDirectoryRecursive(installer.OPENSEARCH_PLUGINS_DIR); + deleteDirectoryRecursive(installer.OPENSEARCH_LIB_PATH); + deleteDirectoryRecursive(installer.OPENSEARCH_CONF_DIR); + } + + static void setWritePermissions(String filePath) { + if (!installer.OS.toLowerCase().contains("win")) { + Path file = Paths.get(filePath); + Set perms = new HashSet<>(); + perms.add(PosixFilePermission.OWNER_WRITE); + try { + Files.setPosixFilePermissions(file, perms); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + private void verifyStdOutContainsString(String s) { + assertThat(outContent.toString(), containsString(s)); + } + + private void verifyStdOutDoesNotContainString(String s) { + assertThat(outContent.toString(), not(containsString(s))); + } +} diff --git a/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java new file mode 100644 index 0000000000..f4b56e6f76 --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/democonfig/SecuritySettingsConfigurerTests.java @@ -0,0 +1,425 @@ +/* + * 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.tools.democonfig; + +// CS-SUPPRESS-SINGLE: RegexpSingleline extension key-word is used in file ext variable +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.apache.commons.lang3.RandomStringUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.tools.Hasher; +import org.opensearch.security.tools.democonfig.util.NoExitSecurityManager; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_INVALID_REGEX; +import static org.opensearch.security.dlic.rest.validation.RequestContentValidator.ValidationError.INVALID_PASSWORD_TOO_SHORT; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_ADMIN_PASSWORD; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.DEFAULT_PASSWORD_MIN_LENGTH; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.REST_ENABLED_ROLES; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.SYSTEM_INDICES; +import static org.opensearch.security.tools.democonfig.SecuritySettingsConfigurer.isKeyPresentInYMLFile; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createDirectory; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.createFile; +import static org.opensearch.security.tools.democonfig.util.DemoConfigHelperUtil.deleteDirectoryRecursive; +import static org.junit.Assert.fail; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +public class SecuritySettingsConfigurerTests { + + private final ByteArrayOutputStream outContent = new ByteArrayOutputStream(); + private final PrintStream originalOut = System.out; + private final PrintStream originalErr = System.err; + private final InputStream originalIn = System.in; + + private final String adminPasswordKey = ConfigConstants.OPENSEARCH_INITIAL_ADMIN_PASSWORD; + + private static final String PASSWORD_VALIDATION_FAILURE_MESSAGE = + "Password %s failed validation: \"%s\". Please re-try with a minimum %d character password and must contain at least one uppercase letter, one lowercase letter, one digit, and one special character that is strong. Password strength can be tested here: https://lowe.github.io/tryzxcvbn"; + + private static SecuritySettingsConfigurer securitySettingsConfigurer; + + private static Installer installer; + + @Before + public void setUp() throws IOException { + System.setOut(new PrintStream(outContent)); + System.setErr(new PrintStream(outContent)); + installer = Installer.getInstance(); + installer.buildOptions(); + securitySettingsConfigurer = new SecuritySettingsConfigurer(installer); + setUpConf(); + setUpInternalUsersYML(); + } + + @After + public void tearDown() throws NoSuchFieldException, IllegalAccessException { + outContent.reset(); + System.setOut(originalOut); + System.setErr(originalErr); + System.setIn(originalIn); + deleteDirectoryRecursive(installer.OPENSEARCH_CONF_DIR); + unsetEnvVariables(); + Installer.resetInstance(); + } + + @Test + public void testUpdateAdminPasswordWithCustomPassword() throws NoSuchFieldException, IllegalAccessException, IOException { + String customPassword = "myStrongPassword123"; + setEnv(adminPasswordKey, customPassword); + + securitySettingsConfigurer.updateAdminPassword(); + + assertThat(customPassword, is(equalTo(SecuritySettingsConfigurer.ADMIN_PASSWORD))); + + verifyStdOutContainsString("Admin password set successfully."); + } + + @Test + public void testUpdateAdminPassword_noPasswordSupplied() { + SecuritySettingsConfigurer.ADMIN_PASSWORD = ""; // to ensure 0 flaky-ness + try { + System.setSecurityManager(new NoExitSecurityManager()); + securitySettingsConfigurer.updateAdminPassword(); + } catch (SecurityException | IOException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString( + String.format( + "No custom admin password found. Please provide a password via the environment variable %s.", + ConfigConstants.OPENSEARCH_INITIAL_ADMIN_PASSWORD + ) + ); + } + + @Test + public void testUpdateAdminPasswordWithWeakPassword() throws NoSuchFieldException, IllegalAccessException { + + setEnv(adminPasswordKey, "weakpassword"); + try { + System.setSecurityManager(new NoExitSecurityManager()); + securitySettingsConfigurer.updateAdminPassword(); + } catch (SecurityException | IOException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString( + String.format( + PASSWORD_VALIDATION_FAILURE_MESSAGE, + "weakpassword", + INVALID_PASSWORD_INVALID_REGEX.message(), + DEFAULT_PASSWORD_MIN_LENGTH + ) + ); + } + + @Test + public void testUpdateAdminPasswordWithShortPassword() throws NoSuchFieldException, IllegalAccessException { + + setEnv(adminPasswordKey, "short"); + try { + System.setSecurityManager(new NoExitSecurityManager()); + securitySettingsConfigurer.updateAdminPassword(); + } catch (SecurityException | IOException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString( + String.format(PASSWORD_VALIDATION_FAILURE_MESSAGE, "short", INVALID_PASSWORD_TOO_SHORT.message(), DEFAULT_PASSWORD_MIN_LENGTH) + ); + } + + @Test + public void testUpdateAdminPasswordWithWeakPassword_skipPasswordValidation() throws NoSuchFieldException, IllegalAccessException, + IOException { + setEnv(adminPasswordKey, "weakpassword"); + installer.environment = ExecutionEnvironment.TEST; + securitySettingsConfigurer.updateAdminPassword(); + + assertThat("weakpassword", is(equalTo(SecuritySettingsConfigurer.ADMIN_PASSWORD))); + + verifyStdOutContainsString("Admin password set successfully."); + } + + @Test + public void testUpdateAdminPasswordWithCustomInternalUsersYML() throws IOException { + String internalUsersFile = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; + Path internalUsersFilePath = Paths.get(internalUsersFile); + + List newContent = Arrays.asList( + "_meta:", + " type: \"internalusers\"", + " config_version: 2", + "admin:", + " hash: " + Hasher.hash(RandomStringUtils.randomAlphanumeric(16).toCharArray()), + " backend_roles:", + " - \"admin\"" + ); + // overwriting existing content + Files.write(internalUsersFilePath, newContent, StandardCharsets.UTF_8); + + securitySettingsConfigurer.updateAdminPassword(); + + verifyStdOutContainsString("Admin password seems to be custom configured. Skipping update to admin password."); + } + + @Test + public void testUpdateAdminPasswordWithDefaultInternalUsersYml() { + + SecuritySettingsConfigurer.ADMIN_PASSWORD = ""; // to ensure 0 flaky-ness + try { + System.setSecurityManager(new NoExitSecurityManager()); + securitySettingsConfigurer.updateAdminPassword(); + } catch (SecurityException | IOException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString( + String.format( + "No custom admin password found. Please provide a password via the environment variable %s.", + ConfigConstants.OPENSEARCH_INITIAL_ADMIN_PASSWORD + ) + ); + } + + @Test + public void testSecurityPluginAlreadyConfigured() { + securitySettingsConfigurer.writeSecurityConfigToOpenSearchYML(); + String expectedMessage = installer.OPENSEARCH_CONF_FILE + " seems to be already configured for Security. Quit."; + try { + System.setSecurityManager(new NoExitSecurityManager()); + securitySettingsConfigurer.checkIfSecurityPluginIsAlreadyConfigured(); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + verifyStdOutContainsString(expectedMessage); + } + + @Test + public void testSecurityPluginNotConfigured() { + try { + securitySettingsConfigurer.checkIfSecurityPluginIsAlreadyConfigured(); + } catch (Exception e) { + fail("Expected checkIfSecurityPluginIsAlreadyConfigured to succeed without any errors."); + } + } + + @Test + public void testConfigFileDoesNotExist() { + installer.OPENSEARCH_CONF_FILE = "path/to/nonexistentfile"; + String expectedMessage = "OpenSearch configuration file does not exist. Quit."; + try { + System.setSecurityManager(new NoExitSecurityManager()); + securitySettingsConfigurer.checkIfSecurityPluginIsAlreadyConfigured(); + } catch (SecurityException e) { + assertThat(e.getMessage(), equalTo("System.exit(-1) blocked to allow print statement testing.")); + } finally { + System.setSecurityManager(null); + } + + verifyStdOutContainsString(expectedMessage); + + // reset the file pointer + installer.OPENSEARCH_CONF_FILE = installer.OPENSEARCH_CONF_DIR + "opensearch.yml"; + } + + @Test + public void testBuildSecurityConfigMap() { + Map actual = securitySettingsConfigurer.buildSecurityConfigMap(); + + assertThat(actual.size(), is(17)); + assertThat(actual.get("plugins.security.ssl.transport.pemcert_filepath"), is(equalTo(Certificates.NODE_CERT.getFileName()))); + assertThat(actual.get("plugins.security.ssl.transport.pemkey_filepath"), is(equalTo(Certificates.NODE_KEY.getFileName()))); + assertThat(actual.get("plugins.security.ssl.transport.pemtrustedcas_filepath"), is(equalTo(Certificates.ROOT_CA.getFileName()))); + assertThat(actual.get("plugins.security.ssl.transport.enforce_hostname_verification"), is(equalTo(false))); + assertThat(actual.get("plugins.security.ssl.http.enabled"), is(equalTo(true))); + assertThat(actual.get("plugins.security.ssl.http.pemcert_filepath"), is(equalTo(Certificates.NODE_CERT.getFileName()))); + assertThat(actual.get("plugins.security.ssl.http.pemkey_filepath"), is(equalTo(Certificates.NODE_KEY.getFileName()))); + assertThat(actual.get("plugins.security.ssl.http.pemtrustedcas_filepath"), is(equalTo(Certificates.ROOT_CA.getFileName()))); + assertThat(actual.get("plugins.security.allow_unsafe_democertificates"), is(equalTo(true))); + assertThat(actual.get("plugins.security.authcz.admin_dn"), is(equalTo(List.of("CN=kirk,OU=client,O=client,L=test,C=de")))); + assertThat(actual.get("plugins.security.audit.type"), is(equalTo("internal_opensearch"))); + assertThat(actual.get("plugins.security.enable_snapshot_restore_privilege"), is(equalTo(true))); + assertThat(actual.get("plugins.security.check_snapshot_restore_write_privileges"), is(equalTo(true))); + assertThat(actual.get("plugins.security.restapi.roles_enabled"), is(equalTo(REST_ENABLED_ROLES))); + assertThat(actual.get("plugins.security.system_indices.enabled"), is(equalTo(true))); + assertThat(actual.get("plugins.security.system_indices.indices"), is(equalTo(SYSTEM_INDICES))); + assertThat(actual.get("node.max_local_storage_nodes"), is(equalTo(3))); + + installer.initsecurity = true; + actual = securitySettingsConfigurer.buildSecurityConfigMap(); + assertThat(actual.get("plugins.security.allow_default_init_securityindex"), is(equalTo(true))); + + installer.cluster_mode = true; + actual = securitySettingsConfigurer.buildSecurityConfigMap(); + assertThat(actual.get("network.host"), is(equalTo("0.0.0.0"))); + assertThat(actual.get("node.name"), is(equalTo("smoketestnode"))); + assertThat(actual.get("cluster.initial_cluster_manager_nodes"), is(equalTo("smoketestnode"))); + } + + @Test + public void testIsStringAlreadyPresentInFile_isNotPresent() throws IOException { + String str1 = "network.host"; + String str2 = "some.random.config"; + + installer.initsecurity = true; + securitySettingsConfigurer.writeSecurityConfigToOpenSearchYML(); + + assertThat(isKeyPresentInYMLFile(installer.OPENSEARCH_CONF_FILE, str1), is(equalTo(false))); + assertThat(isKeyPresentInYMLFile(installer.OPENSEARCH_CONF_FILE, str2), is(equalTo(false))); + } + + @Test + public void testIsStringAlreadyPresentInFile_isPresent() throws IOException { + String str1 = "network.host"; + String str2 = "some.random.config"; + + installer.initsecurity = true; + installer.cluster_mode = true; + securitySettingsConfigurer.writeSecurityConfigToOpenSearchYML(); + + assertThat(isKeyPresentInYMLFile(installer.OPENSEARCH_CONF_FILE, str1), is(equalTo(true))); + assertThat(isKeyPresentInYMLFile(installer.OPENSEARCH_CONF_FILE, str2), is(equalTo(false))); + } + + @Test + public void testAssumeYesDoesNotInitializeClusterMode() throws IOException { + String nodeName = "node.name"; // cluster_mode + String securityIndex = "plugins.security.allow_default_init_securityindex"; // init_security + + installer.assumeyes = true; + securitySettingsConfigurer.writeSecurityConfigToOpenSearchYML(); + + assertThat(isKeyPresentInYMLFile(installer.OPENSEARCH_CONF_FILE, nodeName), is(false)); + assertThat(isKeyPresentInYMLFile(installer.OPENSEARCH_CONF_FILE, securityIndex), is(false)); + } + + @Test + public void testCreateSecurityAdminDemoScriptAndGetSecurityAdminCommands() throws IOException { + String demoPath = installer.OPENSEARCH_CONF_DIR + "securityadmin_demo" + installer.FILE_EXTENSION; + securitySettingsConfigurer.createSecurityAdminDemoScript("scriptPath", demoPath); + + assertThat(new File(demoPath).exists(), is(equalTo(true))); + + String[] commands = securitySettingsConfigurer.getSecurityAdminCommands("scriptPath"); + + try (BufferedReader reader = new BufferedReader(new FileReader(demoPath, StandardCharsets.UTF_8))) { + assertThat(reader.readLine(), is(commands[0])); + assertThat(reader.readLine(), is(equalTo(commands[1]))); + } + } + + @Test + public void testCreateSecurityAdminDemoScript_invalidPath() { + String demoPath = null; + try { + securitySettingsConfigurer.createSecurityAdminDemoScript("scriptPath", demoPath); + fail("Expected to throw Exception"); + } catch (IOException | NullPointerException e) { + // expected + } + } + + @SuppressWarnings("unchecked") + public static void setEnv(String key, String value) throws NoSuchFieldException, IllegalAccessException { + Class[] classes = Collections.class.getDeclaredClasses(); + Map env = System.getenv(); + for (Class cl : classes) { + if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Object obj = field.get(env); + Map map = (Map) obj; + map.clear(); + map.put(key, value); + } + } + } + + @SuppressWarnings("unchecked") + public static void unsetEnvVariables() throws NoSuchFieldException, IllegalAccessException { + Class[] classes = Collections.class.getDeclaredClasses(); + Map env = System.getenv(); + for (Class cl : classes) { + if ("java.util.Collections$UnmodifiableMap".equals(cl.getName())) { + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Object obj = field.get(env); + Map map = (Map) obj; + map.clear(); + } + } + } + + void setUpConf() { + installer.OPENSEARCH_CONF_DIR = System.getProperty("user.dir") + File.separator + "test-conf" + File.separator; + installer.OPENSEARCH_CONF_FILE = installer.OPENSEARCH_CONF_DIR + "opensearch.yml"; + String securityConfDir = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator; + createDirectory(securityConfDir); + createFile(securityConfDir + "internal_users.yml"); + createFile(installer.OPENSEARCH_CONF_FILE); + } + + private void verifyStdOutContainsString(String s) { + assertThat(outContent.toString(), containsString(s)); + } + + private void setUpInternalUsersYML() throws IOException { + String internalUsersFile = installer.OPENSEARCH_CONF_DIR + "opensearch-security" + File.separator + "internal_users.yml"; + Path internalUsersFilePath = Paths.get(internalUsersFile); + List defaultContent = Arrays.asList( + "_meta:", + " type: \"internalusers\"", + " config_version: 2", + "admin:", + " hash: " + Hasher.hash(DEFAULT_ADMIN_PASSWORD.toCharArray()), + " reserved: " + true, + " backend_roles:", + " - \"admin\"", + " description: Demo admin user" + ); + Files.write(internalUsersFilePath, defaultContent, StandardCharsets.UTF_8); + } +} diff --git a/src/test/java/org/opensearch/security/tools/democonfig/util/DemoConfigHelperUtil.java b/src/test/java/org/opensearch/security/tools/democonfig/util/DemoConfigHelperUtil.java new file mode 100644 index 0000000000..7fd4c3330d --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/democonfig/util/DemoConfigHelperUtil.java @@ -0,0 +1,54 @@ +/* + * 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.tools.democonfig.util; + +import java.io.File; + +public class DemoConfigHelperUtil { + public static void createDirectory(String path) { + File directory = new File(path); + if (!directory.exists() && !directory.mkdirs()) { + throw new RuntimeException("Failed to create directory: " + path); + } + } + + public static void createFile(String path) { + try { + File file = new File(path); + if (!file.exists() && !file.createNewFile()) { + throw new RuntimeException("Failed to create file: " + path); + } + } catch (Exception e) { + // without this the catch, we would need to throw exception, + // which would then require modifying caller method signature + throw new RuntimeException("Failed to create file: " + path, e); + } + } + + public static void deleteDirectoryRecursive(String path) { + File directory = new File(path); + if (directory.exists()) { + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectoryRecursive(file.getAbsolutePath()); + } else { + file.delete(); + } + } + } + // Delete the empty directory after all its content is deleted + directory.delete(); + } + } +} diff --git a/src/test/java/org/opensearch/security/tools/democonfig/util/NoExitSecurityManager.java b/src/test/java/org/opensearch/security/tools/democonfig/util/NoExitSecurityManager.java new file mode 100644 index 0000000000..0602812f5d --- /dev/null +++ b/src/test/java/org/opensearch/security/tools/democonfig/util/NoExitSecurityManager.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.tools.democonfig.util; + +/** + * Helper class to allow capturing and testing exit codes and block test execution from exiting mid-way + */ +public class NoExitSecurityManager extends SecurityManager { + @Override + public void checkPermission(java.security.Permission perm) { + // Allow everything except System.exit code 0 & -1 + if (perm instanceof java.lang.RuntimePermission && ("exitVM.0".equals(perm.getName()) || "exitVM.-1".equals(perm.getName()))) { + StringBuilder sb = new StringBuilder(); + sb.append("System.exit("); + sb.append(perm.getName().contains("0") ? 0 : -1); + sb.append(") blocked to allow print statement testing."); + throw new SecurityException(sb.toString()); + } + } +} diff --git a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java index 903ad89eac..8d902ed498 100644 --- a/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java +++ b/src/test/java/org/opensearch/security/transport/SecurityInterceptorTests.java @@ -10,7 +10,11 @@ // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used for creating a mock import java.net.InetAddress; +import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; import org.junit.Before; import org.junit.Test; @@ -51,6 +55,9 @@ import static java.util.Collections.emptySet; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -82,12 +89,44 @@ public class SecurityInterceptorTests { @Mock private SSLConfig sslConfig; - private Settings settings; + @Mock + private TransportRequest request; + + @Mock + private TransportRequestOptions options; + + @SuppressWarnings("unchecked") + private TransportResponseHandler handler = mock(TransportResponseHandler.class); + private Settings settings; private ThreadPool threadPool; + private ClusterName clusterName = ClusterName.DEFAULT; + private MockTransport transport; + private TransportService transportService; + private OpenSearchSecurityPlugin.GuiceHolder guiceHolder; + private User user; + private String action = "testAction"; + private Version remoteNodeVersion = Version.V_2_0_0; + + private InetAddress localAddress; + private InetAddress remoteAddress; + private DiscoveryNode localNode; + private Connection connection1; + private DiscoveryNode otherNode; + private Connection connection2; + private DiscoveryNode remoteNode; + private Connection connection3; + private DiscoveryNode otherRemoteNode; + private Connection connection4; + + private AsyncSender sender; + private AsyncSender serializedSender; + private AtomicReference senderLatch = new AtomicReference<>(new CountDownLatch(1)); @Before public void setup() { + + // Build mocked objects MockitoAnnotations.openMocks(this); settings = Settings.builder() .put("node.name", SecurityInterceptorTests.class.getSimpleName()) @@ -104,17 +143,15 @@ public void setup() { clusterService, sslExceptionHandler, clusterInfoHolder, - sslConfig + sslConfig, + () -> true ); - } - private void testSendRequestDecorate(Version remoteNodeVersion) { - boolean useJDKSerialization = remoteNodeVersion.before(ConfigConstants.FIRST_CUSTOM_SERIALIZATION_SUPPORTED_OS_VERSION); - ClusterName clusterName = ClusterName.DEFAULT; + clusterName = ClusterName.DEFAULT; when(clusterService.getClusterName()).thenReturn(clusterName); - MockTransport transport = new MockTransport(); - TransportService transportService = transport.createTransportService( + transport = new MockTransport(); + transportService = transport.createTransportService( Settings.EMPTY, threadPool, TransportService.NOOP_TRANSPORT_INTERCEPTOR, @@ -125,7 +162,7 @@ private void testSendRequestDecorate(Version remoteNodeVersion) { ); // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used for creating a mock - OpenSearchSecurityPlugin.GuiceHolder guiceHolder = new OpenSearchSecurityPlugin.GuiceHolder( + guiceHolder = new OpenSearchSecurityPlugin.GuiceHolder( mock(RepositoriesService.class), transportService, mock(IndicesService.class), @@ -134,30 +171,34 @@ private void testSendRequestDecorate(Version remoteNodeVersion) { ); // CS-ENFORCE-SINGLE - User user = new User("John Doe"); - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + // Instantiate objects for tests + user = new User("John Doe"); - String action = "testAction"; - TransportRequest request = mock(TransportRequest.class); - TransportRequestOptions options = mock(TransportRequestOptions.class); - @SuppressWarnings("unchecked") - TransportResponseHandler handler = mock(TransportResponseHandler.class); + request = mock(TransportRequest.class); + options = mock(TransportRequestOptions.class); - InetAddress localAddress = null; + localAddress = null; + remoteAddress = null; try { localAddress = InetAddress.getByName("0.0.0.0"); + remoteAddress = InetAddress.getByName("1.1.1.1"); } catch (final UnknownHostException uhe) { throw new RuntimeException(uhe); } - DiscoveryNode localNode = new DiscoveryNode("local-node", new TransportAddress(localAddress, 1234), Version.CURRENT); - Connection connection1 = transportService.getConnection(localNode); + localNode = new DiscoveryNode("local-node1", new TransportAddress(localAddress, 1234), Version.CURRENT); + connection1 = transportService.getConnection(localNode); - DiscoveryNode otherNode = new DiscoveryNode("remote-node", new TransportAddress(localAddress, 4321), remoteNodeVersion); - Connection connection2 = transportService.getConnection(otherNode); + otherNode = new DiscoveryNode("local-node2", new TransportAddress(localAddress, 4321), Version.CURRENT); + connection2 = transportService.getConnection(otherNode); - // from thread context inside sendRequestDecorate - AsyncSender sender = new AsyncSender() { + remoteNode = new DiscoveryNode("remote-node", new TransportAddress(localAddress, 6789), remoteNodeVersion); + connection3 = transportService.getConnection(remoteNode); + + otherRemoteNode = new DiscoveryNode("remote-node2", new TransportAddress(remoteAddress, 9876), remoteNodeVersion); + connection4 = transportService.getConnection(otherRemoteNode); + + serializedSender = new AsyncSender() { @Override public void sendRequest( Connection connection, @@ -166,19 +207,12 @@ public void sendRequest( TransportRequestOptions options, TransportResponseHandler handler ) { - User transientUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - assertEquals(transientUser, user); + String serializedUserHeader = threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER); + assertEquals(serializedUserHeader, Base64Helper.serializeObject(user, true)); + senderLatch.get().countDown(); } }; - // isSameNodeRequest = true - securityInterceptor.sendRequestDecorate(sender, connection1, action, request, options, handler, localNode); - - // 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); - // checking thread context inside sendRequestDecorate sender = new AsyncSender() { @Override public void sendRequest( @@ -188,30 +222,172 @@ public void sendRequest( TransportRequestOptions options, TransportResponseHandler handler ) { - String serializedUserHeader = threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER); - assertEquals(serializedUserHeader, Base64Helper.serializeObject(user, useJDKSerialization)); + User transientUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + assertEquals(transientUser, user); + senderLatch.get().countDown(); } }; - // 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); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + } + + /** + * A method to confirm the original thread context is maintained + * @param user The expected user to be in the transient header + */ + final void verifyOriginalContext(User user) { + + User transientUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + assertEquals(transientUser, user); + assertNull(threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER)); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + final void completableRequestDecorate( + AsyncSender sender, + Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler, + DiscoveryNode localNode + ) { + securityInterceptor.sendRequestDecorate(sender, connection, action, request, options, handler, localNode); + verifyOriginalContext(user); + try { + senderLatch.get().await(1, TimeUnit.SECONDS); + } catch (final InterruptedException e) { + throw new RuntimeException(e); + } + + // Reset the latch so another request can be processed + senderLatch.set(new CountDownLatch(1)); } @Test - public void testSendRequestDecorate() { - testSendRequestDecorate(Version.CURRENT); + public void testSendRequestDecorateLocalConnection() { + + // local node request + completableRequestDecorate(sender, connection1, action, request, options, handler, localNode); + // this is also a local request + completableRequestDecorate(sender, connection2, action, request, options, handler, otherNode); + } + + @Test + public void testSendRequestDecorateRemoteConnection() { + + // this is a remote request + completableRequestDecorate(serializedSender, connection3, action, request, options, handler, localNode); + // this is a remote request where the transport address is different + completableRequestDecorate(serializedSender, connection4, action, request, options, handler, localNode); + } + + @Test + public void testSendNoOriginNodeCausesSerialization() { + + // this is a request where the local node is null; have to use the remote connection since the serialization will fail + completableRequestDecorate(serializedSender, connection3, action, request, options, handler, null); + } + + @Test + public void testSendNoConnectionShouldThrowNPE() { + + // The completable version swallows the NPE so have to call actual method + assertThrows( + java.lang.NullPointerException.class, + () -> securityInterceptor.sendRequestDecorate(serializedSender, null, action, request, options, handler, localNode) + ); + } + + @Test + public void testNullOriginHeaderCausesNoSerialization() { + + // Make the origin null should cause the ensureCorrectHeaders method to populate with Origin.LOCAL.toString() + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, null); + // This is a different way to get the same result which exercises the origin0 = null logic of ensureCorrectHeaders + securityInterceptor.sendRequestDecorate(sender, connection1, action, request, options, handler, localNode); + verifyOriginalContext(user); + } + + @Test + public void testNullRemoteAddressCausesNoSerialization() { + + // Make the remote address null should cause the ensureCorrectHeaders to keep the TransportAddress as null ultimately causing local + // logic to occur + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, null); + // This is a different way to get the same result which exercises the origin0 = null logic of ensureCorrectHeaders + completableRequestDecorate(sender, connection1, action, request, options, handler, localNode); + } + + @Test + public void testCustomRemoteAddressCausesSerialization() { + + threadPool.getThreadContext() + .putHeader( + ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, + String.valueOf(new TransportAddress(new InetSocketAddress("8.8.8.8", 80))) + ); + completableRequestDecorate(serializedSender, connection3, action, request, options, handler, localNode); + } + + @Test + public void testTraceHeaderIsRemoved() { + + threadPool.getThreadContext().putTransient("_opendistro_security_trace", "fake trace value"); + // this case is just for action trace logic validation + // local node request + completableRequestDecorate(sender, connection1, action, request, options, handler, localNode); + // even though we add the trace the restoring handler should remove it from the thread context + assertFalse( + threadPool.getThreadContext().getHeaders().keySet().stream().anyMatch(header -> header.startsWith("_opendistro_security_trace")) + ); } - /** - * 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); + public void testFakeHeaderIsIgnored() { + + threadPool.getThreadContext().putHeader("FAKE_HEADER", "fake_value"); + // this is a local request + completableRequestDecorate(sender, connection1, action, request, options, handler, localNode); + // this is a remote request + completableRequestDecorate(serializedSender, connection3, action, request, options, handler, localNode); } + @Test + public void testNullHeaderIsIgnored() { + + // Add a null header + threadPool.getThreadContext().putHeader(null, null); + threadPool.getThreadContext().putHeader(null, "null"); + // this is a local request + completableRequestDecorate(sender, connection1, action, request, options, handler, localNode); + // this is a remote request + completableRequestDecorate(serializedSender, connection3, action, request, options, handler, localNode); + } + + @Test + public void testFakeHeadersAreIgnored() { + + threadPool.getThreadContext() + .putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "fake security config request header"); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER, "fake security origin header"); + threadPool.getThreadContext() + .putHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER, "fake security remote address header"); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER, "fake dls query header"); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER, "fake fls fields header"); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER, "fake masked field header"); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER, "fake doc allowlist header"); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_FILTER_LEVEL_DLS_DONE, "fake filter level dls header"); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_MODE_HEADER, "fake dls mode header"); + threadPool.getThreadContext() + .putHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_HEADER, "fake dls filter header"); + threadPool.getThreadContext() + .putHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER, "fake initial action header"); + threadPool.getThreadContext().putHeader("_opendistro_security_source_field_context", "fake source field context value"); + threadPool.getThreadContext() + .putHeader(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION, "fake injected roles validation string"); + + // this is a local request + completableRequestDecorate(sender, connection1, action, request, options, handler, localNode); + } } diff --git a/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java b/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java index b6967b0e68..c63c8d26ae 100644 --- a/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java +++ b/src/test/java/org/opensearch/security/transport/SecuritySSLRequestHandlerTests.java @@ -9,12 +9,15 @@ */ package org.opensearch.security.transport; +import java.io.IOException; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.opensearch.Version; import org.opensearch.common.settings.Settings; +import org.opensearch.core.transport.TransportResponse; import org.opensearch.security.ssl.SslExceptionHandler; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.ssl.transport.SSLConfig; @@ -27,11 +30,13 @@ import org.opensearch.transport.TransportRequestHandler; import org.mockito.ArgumentMatchers; +import org.mockito.InOrder; import org.mockito.Mock; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -88,9 +93,106 @@ public void testUseJDKSerializationHeaderIsSetOnMessageReceived() throws Excepti 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_2_13_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_2_14_0); + 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.assertTrue(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + } + + @Test + public void testUseJDKSerializationHeaderIsSetWithWrapperChannel() throws Exception { + TransportRequest transportRequest = mock(TransportRequest.class); + TransportChannel transportChannel = mock(TransportChannel.class); + TransportChannel wrappedChannel = new WrappedTransportChannel(transportChannel); + Task task = mock(Task.class); + doNothing().when(transportChannel).sendResponse(ArgumentMatchers.any(Exception.class)); + when(transportChannel.getVersion()).thenReturn(Version.V_2_10_0); + when(transportChannel.getChannelType()).thenReturn("other"); + + Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, wrappedChannel, 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, wrappedChannel, task)); Assert.assertFalse(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + + threadPool.getThreadContext().stashContext(); + when(transportChannel.getVersion()).thenReturn(Version.V_2_13_0); + Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, wrappedChannel, task)); + Assert.assertFalse(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + + threadPool.getThreadContext().stashContext(); + when(transportChannel.getVersion()).thenReturn(Version.V_2_14_0); + Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, wrappedChannel, 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, wrappedChannel, task)); + Assert.assertTrue(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + } + + @Test + public void testUseJDKSerializationHeaderIsSetAfterGetInnerChannel() throws Exception { + TransportRequest transportRequest = mock(TransportRequest.class); + TransportChannel transportChannel = mock(TransportChannel.class); + WrappedTransportChannel wrappedChannel = mock(WrappedTransportChannel.class); + Task task = mock(Task.class); + when(wrappedChannel.getInnerChannel()).thenReturn(transportChannel); + when(wrappedChannel.getChannelType()).thenReturn("other"); + doNothing().when(transportChannel).sendResponse(ArgumentMatchers.any(Exception.class)); + when(transportChannel.getVersion()).thenReturn(Version.V_2_10_0); + + Assert.assertThrows(Exception.class, () -> securitySSLRequestHandler.messageReceived(transportRequest, wrappedChannel, task)); + Assert.assertTrue(threadPool.getThreadContext().getTransient(ConfigConstants.USE_JDK_SERIALIZATION)); + + InOrder inOrder = inOrder(wrappedChannel, transportChannel); + + inOrder.verify(wrappedChannel).getInnerChannel(); + inOrder.verify(transportChannel).getVersion(); + } + + public class WrappedTransportChannel implements TransportChannel { + + private TransportChannel inner; + + public WrappedTransportChannel(TransportChannel inner) { + this.inner = inner; + } + + @Override + public String getProfileName() { + return "WrappedTransportChannelProfileName"; + } + + public TransportChannel getInnerChannel() { + return this.inner; + } + + @Override + public void sendResponse(TransportResponse response) throws IOException { + inner.sendResponse(response); + } + + @Override + public void sendResponse(Exception e) throws IOException { + + } + + @Override + public String getChannelType() { + return "WrappedTransportChannelType"; + } } } diff --git a/src/test/java/org/opensearch/security/util/Repeat.java b/src/test/java/org/opensearch/security/util/Repeat.java new file mode 100644 index 0000000000..be9c9fddf0 --- /dev/null +++ b/src/test/java/org/opensearch/security/util/Repeat.java @@ -0,0 +1,25 @@ +/* + * 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 java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.METHOD; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ METHOD, ANNOTATION_TYPE }) +public @interface Repeat { + int value() default 1; +} diff --git a/src/test/java/org/opensearch/security/util/RepeatRule.java b/src/test/java/org/opensearch/security/util/RepeatRule.java new file mode 100644 index 0000000000..387f0c17e5 --- /dev/null +++ b/src/test/java/org/opensearch/security/util/RepeatRule.java @@ -0,0 +1,48 @@ +/* + * 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.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +public class RepeatRule implements TestRule { + + private static class RepeatStatement extends Statement { + private final Statement statement; + private final int repeat; + + public RepeatStatement(Statement statement, int repeat) { + this.statement = statement; + this.repeat = repeat; + } + + @Override + public void evaluate() throws Throwable { + for (int i = 0; i < repeat; i++) { + statement.evaluate(); + } + } + + } + + @Override + public Statement apply(Statement statement, Description description) { + Statement result = statement; + Repeat repeat = description.getAnnotation(Repeat.class); + if (repeat != null) { + int times = repeat.value(); + result = new RepeatStatement(statement, times); + } + return result; + } +} diff --git a/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml b/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml index 046e4d6ee5..027ee6869e 100644 --- a/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml +++ b/src/test/resources/auditlog/endpoints/routing/configuration_valid.yml @@ -15,7 +15,23 @@ plugins.security: endpoint2: type: external_opensearch config: - http_endpoints: ['localhost:9200','localhost:9201','localhost:9202'] + http_endpoints: [ + 'localhost', + 'localhost:9200', + 'localhost:9201', + 'localhost:9202', + 'localhost:9202/opensearch', + '127.0.0.1', + '127.0.0.1:9200', + '127.0.0.1:9200/opensearch', + 'my-opensearch-cluster.company.com:9200', + 'my-opensearch-cluster.company.com:9200/opensearch', + 'http://my-opensearch-cluster.company.com', + 'https://my-opensearch-cluster.company.com:9200', + 'https://my-opensearch-cluster.company.com:9200/opensearch', + '[::1]:9200', + '[::1]:9200/opensearch', + ] index: auditlog username: auditloguser password: auditlogpassword diff --git a/src/test/resources/auditlog/endpoints/sink/configuration_all_variants.yml b/src/test/resources/auditlog/endpoints/sink/configuration_all_variants.yml index f1c8620e88..82565ee3ec 100644 --- a/src/test/resources/auditlog/endpoints/sink/configuration_all_variants.yml +++ b/src/test/resources/auditlog/endpoints/sink/configuration_all_variants.yml @@ -45,3 +45,5 @@ plugins.security: config: log4j.logger_name: loggername log4j.level: invalid + endpoint13: + type: log4j diff --git a/src/test/resources/log4j2-test.properties b/src/test/resources/log4j2-test.properties index 3d22ca3765..866b68325c 100644 --- a/src/test/resources/log4j2-test.properties +++ b/src/test/resources/log4j2-test.properties @@ -17,6 +17,12 @@ rootLogger.level = warn rootLogger.appenderRef.console.ref = console rootLogger.appenderRef.file.ref = LOGFILE +# For troubleshooting com.amazon.dlic.auth.ldap.* test cases +logger.ldapServerLogger.name = com.amazon.dlic.auth.ldap.srv.LdapServer.ServerLogger +logger.ldapServerLogger.level = info +logger.ldapAuthBackend.name = com.amazon.dlic.auth.ldap.backend.LDAPAuthorizationBackend +logger.ldapAuthBackend.level = debug + #logger.resolver.name = org.opensearch.security.resolver #logger.resolver.level = trace diff --git a/src/test/resources/restapi/action_groups.yml b/src/test/resources/restapi/action_groups.yml index 638f65f72f..4ec858a69e 100644 --- a/src/test/resources/restapi/action_groups.yml +++ b/src/test/resources/restapi/action_groups.yml @@ -132,3 +132,16 @@ OPENDISTRO_SECURITY_SUGGEST: - "indices:data/read/suggest*" type: "index" description: "Migrated from v6" +rest_admin_action_group: + allowed_actions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' + type: "cluster" diff --git a/src/test/resources/restapi/config.yml b/src/test/resources/restapi/config.yml index 2ed865657a..7a7d3d0e98 100644 --- a/src/test/resources/restapi/config.yml +++ b/src/test/resources/restapi/config.yml @@ -21,6 +21,18 @@ config: internalProxies: "192\\.168\\.0\\.10|192\\.168\\.0\\.11" remoteIpHeader: "x-forwarded-for" authc: + openid_auth_domain: + http_enabled: true + transport_enabled: true + order: 4 + http_authenticator: + type: openid + challenge: false + config: {} + authentication_backend: + type: "noop" + config: {} + description: "Migrated from v6" authentication_domain_kerb: http_enabled: false transport_enabled: false diff --git a/src/test/resources/restapi/invalid_config.json b/src/test/resources/restapi/invalid_config.json index 7bbbf2201f..1d43e1edab 100644 --- a/src/test/resources/restapi/invalid_config.json +++ b/src/test/resources/restapi/invalid_config.json @@ -23,7 +23,6 @@ "authc":{ "authentication_domain_kerb":{ "http_enabled":false, - "transport_enabled":false, "order":3, "http_authenticator":{ "challenge":true, @@ -42,7 +41,6 @@ }, "authentication_domain_clientcert":{ "http_enabled":false, - "transport_enabled":false, "order":1, "http_authenticator":{ "challenge":true, @@ -61,7 +59,6 @@ }, "authentication_domain_proxy":{ "http_enabled":false, - "transport_enabled":false, "order":2, "http_authenticator":{ "challenge":true, @@ -81,7 +78,6 @@ }, "authentication_domain_basic_internal":{ "http_enabled":true, - "transport_enabled":true, "order":0, "http_authenticator":{ "challenge":true, @@ -102,7 +98,6 @@ "authz":{ "roles_from_xxx":{ "http_enabled":false, - "transport_enabled":false, "authorization_backend":{ "type":"xxx", "config":{ @@ -113,7 +108,6 @@ }, "roles_from_myldap":{ "http_enabled":false, - "transport_enabled":false, "authorization_backend":{ "type":"ldap", "config":{ diff --git a/src/test/resources/restapi/roles.yml b/src/test/resources/restapi/roles.yml index 1c3756cb4d..925b051bb3 100644 --- a/src/test/resources/restapi/roles.yml +++ b/src/test/resources/restapi/roles.yml @@ -393,6 +393,32 @@ opendistro_security_role_starfleet_captains: allowed_actions: - "CRUD_UT" tenant_permissions: [] +rest_api_admin_roles_mapping_test_without_mapping: + reserved: true + cluster_permissions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' +rest_api_admin_roles_mapping_test_with_mapping: + reserved: true + cluster_permissions: + - 'restapi:admin/actiongroups' + - 'restapi:admin/allowlist' + - 'restapi:admin/config/update' + - 'restapi:admin/internalusers' + - 'restapi:admin/nodesdn' + - 'restapi:admin/roles' + - 'restapi:admin/rolesmapping' + - 'restapi:admin/ssl/certs/info' + - 'restapi:admin/ssl/certs/reload' + - 'restapi:admin/tenants' rest_api_admin_full_access: reserved: true cluster_permissions: diff --git a/src/test/resources/restapi/roles_mapping.yml b/src/test/resources/restapi/roles_mapping.yml index 8bfe826247..73673ae3ff 100644 --- a/src/test/resources/restapi/roles_mapping.yml +++ b/src/test/resources/restapi/roles_mapping.yml @@ -217,6 +217,9 @@ opendistro_security_role_host2: - "opendistro_security_host_localhost" and_backend_roles: [] description: "Migrated from v6" +rest_api_admin_roles_mapping_test_with_mapping: + reserved: true + users: [a, b] rest_api_admin_full_access: reserved: false hidden: true diff --git a/src/test/resources/restapi/security_config.json b/src/test/resources/restapi/security_config.json index e5c09050cc..30b8611e5a 100644 --- a/src/test/resources/restapi/security_config.json +++ b/src/test/resources/restapi/security_config.json @@ -23,7 +23,6 @@ "authc":{ "authentication_domain_kerb":{ "http_enabled":false, - "transport_enabled":false, "order":3, "http_authenticator":{ "challenge":true, @@ -42,7 +41,6 @@ }, "authentication_domain_clientcert":{ "http_enabled":false, - "transport_enabled":false, "order":1, "http_authenticator":{ "challenge":true, @@ -61,7 +59,6 @@ }, "authentication_domain_proxy":{ "http_enabled":false, - "transport_enabled":false, "order":2, "http_authenticator":{ "challenge":true, @@ -81,7 +78,6 @@ }, "authentication_domain_basic_internal":{ "http_enabled":true, - "transport_enabled":true, "order":0, "http_authenticator":{ "challenge":true, @@ -102,7 +98,6 @@ "authz":{ "roles_from_xxx":{ "http_enabled":false, - "transport_enabled":false, "authorization_backend":{ "type":"xxx", "config":{ @@ -113,7 +108,6 @@ }, "roles_from_myldap":{ "http_enabled":false, - "transport_enabled":false, "authorization_backend":{ "type":"ldap", "config":{ diff --git a/src/test/resources/restapi/securityconfig.json b/src/test/resources/restapi/securityconfig.json index 4e4b1bba63..a577cb2a30 100644 --- a/src/test/resources/restapi/securityconfig.json +++ b/src/test/resources/restapi/securityconfig.json @@ -23,7 +23,6 @@ "authc":{ "authentication_domain_saml": { "http_enabled" : true, - "transport_enabled" : false, "order" : 5, "http_authenticator" : { "challenge" : true, @@ -44,7 +43,6 @@ }, "authentication_domain_kerb":{ "http_enabled":false, - "transport_enabled":false, "order":3, "http_authenticator":{ "challenge":true, @@ -63,7 +61,6 @@ }, "authentication_domain_clientcert":{ "http_enabled":false, - "transport_enabled":false, "order":1, "http_authenticator":{ "challenge":true, @@ -82,7 +79,6 @@ }, "authentication_domain_proxy":{ "http_enabled":false, - "transport_enabled":false, "order":2, "http_authenticator":{ "challenge":true, @@ -102,7 +98,6 @@ }, "authentication_domain_basic_internal":{ "http_enabled":true, - "transport_enabled":true, "order":0, "http_authenticator":{ "challenge":true, @@ -123,7 +118,6 @@ "authz":{ "roles_from_xxx":{ "http_enabled":false, - "transport_enabled":false, "authorization_backend":{ "type":"xxx", "config":{ @@ -134,7 +128,6 @@ }, "roles_from_myldap":{ "http_enabled":false, - "transport_enabled":false, "authorization_backend":{ "type":"ldap", "config":{ diff --git a/src/test/resources/restapi/securityconfig_nondefault.json b/src/test/resources/restapi/securityconfig_nondefault.json index a5660c6496..aae6948b01 100644 --- a/src/test/resources/restapi/securityconfig_nondefault.json +++ b/src/test/resources/restapi/securityconfig_nondefault.json @@ -9,7 +9,8 @@ "private_tenant_enabled" : true, "default_tenant" : "", "server_username" : "kibanaserver", - "index" : ".kibana" + "index" : ".kibana", + "sign_in_options":["BASIC"] }, "http" : { "anonymous_auth_enabled" : false, @@ -22,7 +23,6 @@ "authc" : { "jwt_auth_domain" : { "http_enabled" : true, - "transport_enabled" : true, "order" : 0, "http_authenticator" : { "challenge" : false, @@ -40,7 +40,6 @@ }, "ldap" : { "http_enabled" : false, - "transport_enabled" : false, "order" : 5, "http_authenticator" : { "challenge" : false, @@ -65,7 +64,6 @@ }, "basic_internal_auth_domain" : { "http_enabled" : true, - "transport_enabled" : true, "order" : 4, "http_authenticator" : { "challenge" : true, @@ -80,7 +78,6 @@ }, "proxy_auth_domain" : { "http_enabled" : false, - "transport_enabled" : false, "order" : 3, "http_authenticator" : { "challenge" : false, @@ -98,7 +95,6 @@ }, "clientcert_auth_domain" : { "http_enabled" : false, - "transport_enabled" : false, "order" : 2, "http_authenticator" : { "challenge" : false, @@ -115,7 +111,6 @@ }, "kerberos_auth_domain" : { "http_enabled" : false, - "transport_enabled" : false, "order" : 6, "http_authenticator" : { "challenge" : true, @@ -134,7 +129,6 @@ "authz" : { "roles_from_another_ldap" : { "http_enabled" : false, - "transport_enabled" : false, "authorization_backend" : { "type" : "ldap", "config" : { } @@ -143,7 +137,6 @@ }, "roles_from_myldap" : { "http_enabled" : false, - "transport_enabled" : false, "authorization_backend" : { "type" : "ldap", "config" : { diff --git a/src/test/resources/sanity-tests/kirk-keystore.jks b/src/test/resources/sanity-tests/kirk-keystore.jks index 6dbc51e714..6c8c5ef77e 100644 Binary files a/src/test/resources/sanity-tests/kirk-keystore.jks and b/src/test/resources/sanity-tests/kirk-keystore.jks differ diff --git a/src/test/resources/sanity-tests/root-ca.pem b/src/test/resources/sanity-tests/root-ca.pem index 5948a73b30..854323e6fe 100644 --- a/src/test/resources/sanity-tests/root-ca.pem +++ b/src/test/resources/sanity-tests/root-ca.pem @@ -1,9 +1,9 @@ -----BEGIN CERTIFICATE----- -MIIExjCCA66gAwIBAgIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcNAQEL +MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yMzA4MjkwNDIwMDNaFw0yMzA5MjgwNDIwMDNaMIGPMRMwEQYKCZIm +dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG @@ -18,11 +18,11 @@ F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg -Q29tIEluYy4gUm9vdCBDQYIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcN -AQELBQADggEBAIopqco/k9RSjouTeKP4z0EVUxdD4qnNh1GLSRqyAVe0aChyKF5f -qt1Bd1XCY8D16RgekkKGHDpJhGCpel+vtIoXPBxUaGQNYxmJCf5OzLMODlcrZk5i -jHIcv/FMeK02NBcz/WQ3mbWHVwXLhmwqa2zBsF4FmPCJAbFLchLhkAv1HJifHbnD -jQzlKyl5jxam/wtjWxSm0iyso0z2TgyzY+MESqjEqB1hZkCFzD1xtUOCxbXgtKae -dgfHVFuovr3fNLV3GvQk0s9okDwDUcqV7DSH61e5bUMfE84o3of8YA7+HUoPV5Du -8sTOKRf7ncGXdDRA8aofW268pTCuIu3+g/Y= +Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN +AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC +YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V +6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG +1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq +qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov +rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI= -----END CERTIFICATE----- diff --git a/tools/install_demo_configuration.bat b/tools/install_demo_configuration.bat index d9d30fea2b..5cf4d715fa 100755 --- a/tools/install_demo_configuration.bat +++ b/tools/install_demo_configuration.bat @@ -1,414 +1,29 @@ @echo off -setlocal enableDelayedExpansion -set "SCRIPT_DIR=%~dp0" +set DIR=%~dp0 -echo ************************************************************************** -echo ** This tool will be deprecated in the next major release of OpenSearch ** -echo ** https://github.com/opensearch-project/security/issues/1755 ** -echo ************************************************************************** +set CUR_DIR=%DIR% -echo. -echo OpenSearch Security Demo Installer -echo ** Warning: Do not use on production or public reachable systems ** - -echo. - -set "assumeyes=0" -set "initsecurity=0" -set "cluster_mode=0" -set "skip_updates=-1" - -goto :GETOPTS - -:show_help -echo install_demo_configuration.bat [-y] [-i] [-c] -echo -h show help -echo -y confirm all installation dialogues automatically -echo -i initialize Security plugin with default configuration (default is to ask if -y is not given) -echo -c enable cluster mode by binding to all network interfaces (default is to ask if -y is not given) -echo -s skip updates if config is already applied to opensearch.yml -EXIT /B 0 - -:GETOPTS -if /I "%1" == "-h" call :show_help & exit /b 0 -if /I "%1" == "-y" set "assumeyes=1" -if /I "%1" == "-i" set "initsecurity=1" -if /I "%1" == "-c" set "cluster_mode=1" -if /I "%1" == "-s" set "skip_updates=0" -shift -if not "%1" == "" goto :GETOPTS - -if "%1" == "--" shift - -if %assumeyes% == 0 ( - set /p "response=Install demo certificates? [y/N] " - if /I "!response!" neq "Y" exit /b 0 -) - -if %initsecurity% == 0 ( - if %assumeyes% == 0 ( - set /p "response=Initialize Security Modules? [y/N] " - if /I "!response!" == "Y" (set "initsecurity=1") ELSE (set "initsecurity=0") - ) -) - -if %cluster_mode% == 0 ( - if %assumeyes% == 0 ( - echo Cluster mode requires maybe additional setup of: - echo - Virtual memory [vm.max_map_count] - echo. - set /p "response=Enable cluster mode? [y/N] " - if /I "!response!" == "Y" (set "cluster_mode=1") ELSE (set "cluster_mode=0") - ) -) - -set BASE_DIR=%SCRIPT_DIR%\..\..\..\ -if not exist %BASE_DIR% ( - echo "basedir does not exist" - exit /b 1 -) - -set "CUR=%cd%" -cd %BASE_DIR% -set "BASE_DIR=%cd%\" -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\" -set "OPENSEARCH_MODULES_DIR=%BASE_DIR%modules\" -set "OPENSEARCH_LIB_PATH=%BASE_DIR%lib\" -set "OPENSEARCH_INSTALL_TYPE=.zip" - -if not exist %OPENSEARCH_CONF_FILE% ( - echo Unable to determine OpenSearch config file. Quit. - exit /b 1 -) - -if not exist %OPENSEARCH_BIN_DIR% ( - echo Unable to determine OpenSearch bin directory. Quit. - exit /b 1 -) - -if not exist %OPENSEARCH_PLUGINS_DIR% ( - echo Unable to determine OpenSearch plugins directory. Quit. - exit /b 1 -) - -if not exist %OPENSEARCH_MODULES_DIR% ( - echo Unable to determine OpenSearch modules directory. Quit. - exit /b 1 -) - -if not exist %OPENSEARCH_LIB_PATH% ( - echo Unable to determine OpenSearch lib directory. Quit. - exit /b 1 -) - -if not exist %OPENSEARCH_PLUGINS_DIR%\opensearch-security\ ( - echo OpenSearch Security plugin not installed. Quit. - exit /b 1 +rem set opensearch home for instances when using bundled jdk +if not defined OPENSEARCH_HOME ( + for %%I in ("%DIR%..\..\..") do set "OPENSEARCH_HOME=%%~dpfI" ) +cd %CUR_DIR% -set "OPENSEARCH_VERSION=" -for %%F in ("%OPENSEARCH_LIB_PATH%opensearch-*.jar") do set "OPENSEARCH_VERSION=%%~nxF" & goto :opensearch_version -:opensearch_version -set "OPENSEARCH_JAR_VERSION=" -for /f "tokens=2 delims=[-]" %%a in ("%OPENSEARCH_VERSION%") do set "OPENSEARCH_JAR_VERSION=%%a" - -set "SECURITY_VERSION=" -for %%F in ("%OPENSEARCH_PLUGINS_DIR%\opensearch-security\opensearch-security-*.jar") do set "SECURITY_VERSION=%%~nxF" -set "SECURITY_JAR_VERSION=" -for /f "tokens=3 delims=[-]" %%a in ("%SECURITY_VERSION%") do set "SECURITY_JAR_VERSION=%%a" - -for /f "tokens=4-7 delims=[.] " %%i in ('ver') do (if %%i==Version (set "OS=%%j.%%k") else (set v="%%i.%%j")) -echo OpenSearch install type: %OPENSEARCH_INSTALL_TYPE% on %OS% -echo OpenSearch config dir: %OPENSEARCH_CONF_DIR% -echo OpenSearch config file: %OPENSEARCH_CONF_FILE% -echo OpenSearch bin dir: %OPENSEARCH_BIN_DIR% -echo OpenSearch plugins dir: %OPENSEARCH_PLUGINS_DIR% -echo OpenSearch lib dir: %OPENSEARCH_LIB_PATH% -echo Detected OpenSearch Version: %OPENSEARCH_JAR_VERSION% -echo Detected OpenSearch Security Version: %SECURITY_JAR_VERSION% - ->nul findstr /c:"plugins.security" "%OPENSEARCH_CONF_FILE%" && ( - echo %OPENSEARCH_CONF_FILE% seems to be already configured for Security. Quit. - exit /b %skip_updates% -) - -set LF=^ - - -:: two empty line required after LF -set ADMIN_CERT=-----BEGIN CERTIFICATE-----!LF!^ -MIIEmDCCA4CgAwIBAgIUZjrlDPP8azRDPZchA/XEsx0X2iYwDQYJKoZIhvcNAQEL!LF!^ -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt!LF!^ -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl!LF!^ -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v!LF!^ -dCBDQTAeFw0yMzA4MjkyMDA2MzdaFw0zMzA4MjYyMDA2MzdaME0xCzAJBgNVBAYT!LF!^ -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs!LF!^ -aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC!LF!^ -ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs!LF!^ -paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+!LF!^ -O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx!LF!^ -vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6!LF!^ -cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0!LF!^ -bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw!LF!^ -DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMIHPBgNVHSME!LF!^ -gccwgcSAFBeH36Ba62YSp9XQ+LoSRTy3KwCcoYGVpIGSMIGPMRMwEQYKCZImiZPy!LF!^ -LGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhh!LF!^ -bXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENB!LF!^ -MSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCFHfkrz782p+T9k0G!LF!^ -xGeM4+BrehWKMB0GA1UdDgQWBBSjMS8tgguX/V7KSGLoGg7K6XMzIDANBgkqhkiG!LF!^ -9w0BAQsFAAOCAQEANMwD1JYlwAh82yG1gU3WSdh/tb6gqaSzZK7R6I0L7slaXN9m!LF!^ -y2ErUljpTyaHrdiBFmPhU/2Kj2r+fIUXtXdDXzizx/JdmueT0nG9hOixLqzfoC9p!LF!^ -fAhZxM62RgtyZoaczQN82k1/geMSwRpEndFe3OH7arkS/HSbIFxQhAIy229eWe5d!LF!^ -1bUzP59iu7f3r567I4ob8Vy7PP+Ov35p7Vv4oDHHwgsdRzX6pvL6mmwVrQ3BfVec!LF!^ -h9Dqprr+ukYmjho76g6k5cQuRaB6MxqldzUg+2E7IHQP8MCF+co51uZq2nl33mtp!LF!^ -RGr6JbdHXc96zsLTL3saJQ8AWEfu1gbTVrwyRA==!LF!^ ------END CERTIFICATE-----!LF! - - -set ADMIN_CERT_KEY=-----BEGIN PRIVATE KEY-----!LF!^ -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp!LF!^ -gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky!LF!^ -AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo!LF!^ -7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB!LF!^ -GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+!LF!^ -b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu!LF!^ -y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4!LF!^ -ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0!LF!^ -TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j!LF!^ -xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ!LF!^ -OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo!LF!^ -1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs!LF!^ -9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs!LF!^ -/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3!LF!^ -qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG!LF!^ -/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv!LF!^ -M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0!LF!^ -0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ!LF!^ -K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5!LF!^ -9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF!LF!^ -RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp!LF!^ -nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5!LF!^ -3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h!LF!^ -mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw!LF!^ -F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs!LF!^ -/AHmo368d4PSNRMMzLHw8Q==!LF!^ ------END PRIVATE KEY-----!LF! - - -set NODE_CERT=-----BEGIN CERTIFICATE-----!LF!^ -MIIEPDCCAySgAwIBAgIUZjrlDPP8azRDPZchA/XEsx0X2iIwDQYJKoZIhvcNAQEL!LF!^ -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt!LF!^ -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl!LF!^ -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v!LF!^ -dCBDQTAeFw0yMzA4MjkwNDIzMTJaFw0zMzA4MjYwNDIzMTJaMFcxCzAJBgNVBAYT!LF!^ -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl!LF!^ -MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA!LF!^ -A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud!LF!^ -yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0!LF!^ -HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr!LF!^ -XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n!LF!^ -dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD!LF!^ -ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R!LF!^ -BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA!LF!^ -AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF!LF!^ -BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo!LF!^ -wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ!LF!^ -KoZIhvcNAQELBQADggEBAD2hkndVih6TWxoe/oOW0i2Bq7ScNO/n7/yHWL04HJmR!LF!^ -MaHv/Xjc8zLFLgHuHaRvC02ikWIJyQf5xJt0Oqu2GVbqXH9PBGKuEP2kCsRRyU27!LF!^ -zTclAzfQhqmKBTYQ/3lJ3GhRQvXIdYTe+t4aq78TCawp1nSN+vdH/1geG6QjMn5N!LF!^ -1FU8tovDd4x8Ib/0dv8RJx+n9gytI8n/giIaDCEbfLLpe4EkV5e5UNpOnRgJjjuy!LF!^ -vtZutc81TQnzBtkS9XuulovDE0qI+jQrKkKu8xgGLhgH0zxnPkKtUg2I3Aq6zl1L!LF!^ -zYkEOUF8Y25J6WeY88Yfnc0iigI+Pnz5NK8R9GL7TYo=!LF!^ ------END CERTIFICATE-----!LF! - - -set NODE_KEY=-----BEGIN PRIVATE KEY-----!LF!^ -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv!LF!^ -bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0!LF!^ -o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50!LF!^ -1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1!LF!^ -MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b!LF!^ -6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa!LF!^ -vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo!LF!^ -FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ!LF!^ -5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O!LF!^ -zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ!LF!^ -xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow!LF!^ -dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn!LF!^ -7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U!LF!^ -hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej!LF!^ -VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B!LF!^ -Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c!LF!^ -uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy!LF!^ -hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv!LF!^ -hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/!LF!^ -A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh!LF!^ -KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX!LF!^ -GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f!LF!^ -5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud!LF!^ -tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71!LF!^ -+x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT!LF!^ -bg/ch9Rhxbq22yrVgWHh6epp!LF!^ ------END PRIVATE KEY-----!LF! - - -set ROOT_CA=-----BEGIN CERTIFICATE-----!LF!^ -MIIExjCCA66gAwIBAgIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcNAQEL!LF!^ -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt!LF!^ -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl!LF!^ -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v!LF!^ -dCBDQTAeFw0yMzA4MjkwNDIwMDNaFw0yMzA5MjgwNDIwMDNaMIGPMRMwEQYKCZIm!LF!^ -iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ!LF!^ -RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290!LF!^ -IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG!LF!^ -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU!LF!^ -j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4!LF!^ -U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg!LF!^ -vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA!LF!^ -WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969!LF!^ -VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW!LF!^ -MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU!LF!^ -F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4!LF!^ -uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ!LF!^ -k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD!LF!^ -VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg!LF!^ -Q29tIEluYy4gUm9vdCBDQYIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcN!LF!^ -AQELBQADggEBAIopqco/k9RSjouTeKP4z0EVUxdD4qnNh1GLSRqyAVe0aChyKF5f!LF!^ -qt1Bd1XCY8D16RgekkKGHDpJhGCpel+vtIoXPBxUaGQNYxmJCf5OzLMODlcrZk5i!LF!^ -jHIcv/FMeK02NBcz/WQ3mbWHVwXLhmwqa2zBsF4FmPCJAbFLchLhkAv1HJifHbnD!LF!^ -jQzlKyl5jxam/wtjWxSm0iyso0z2TgyzY+MESqjEqB1hZkCFzD1xtUOCxbXgtKae!LF!^ -dgfHVFuovr3fNLV3GvQk0s9okDwDUcqV7DSH61e5bUMfE84o3of8YA7+HUoPV5Du!LF!^ -8sTOKRf7ncGXdDRA8aofW268pTCuIu3+g/Y=!LF!^ ------END CERTIFICATE-----!LF! - - -echo !ADMIN_CERT! > "%OPENSEARCH_CONF_DIR%kirk.pem" -echo !NODE_CERT! > "%OPENSEARCH_CONF_DIR%esnode.pem" -echo !ROOT_CA! > "%OPENSEARCH_CONF_DIR%root-ca.pem" -echo !NODE_KEY! > "%OPENSEARCH_CONF_DIR%esnode-key.pem" -echo !ADMIN_CERT_KEY! > "%OPENSEARCH_CONF_DIR%kirk-key.pem" - -echo. >> "%OPENSEARCH_CONF_FILE%" -echo ######## Start OpenSearch Security Demo Configuration ######## >> "%OPENSEARCH_CONF_FILE%" -echo # WARNING: revise all the lines below before you go into production >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.transport.pemcert_filepath: esnode.pem >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.transport.pemkey_filepath: esnode-key.pem >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.transport.pemtrustedcas_filepath: root-ca.pem >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.transport.enforce_hostname_verification: false >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.http.enabled: true >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.http.pemcert_filepath: esnode.pem >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.http.pemkey_filepath: esnode-key.pem >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.ssl.http.pemtrustedcas_filepath: root-ca.pem >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.allow_unsafe_democertificates: true >> "%OPENSEARCH_CONF_FILE%" -if %initsecurity% == 1 ( - echo plugins.security.allow_default_init_securityindex: true >> "%OPENSEARCH_CONF_FILE%" -) -echo plugins.security.authcz.admin_dn: >> "%OPENSEARCH_CONF_FILE%" -echo - CN=kirk,OU=client,O=client,L=test, C=de >> "%OPENSEARCH_CONF_FILE%" -echo. >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.audit.type: internal_opensearch >> "%OPENSEARCH_CONF_FILE%" -echo plugins.security.enable_snapshot_restore_privilege: true >> "%OPENSEARCH_CONF_FILE%" -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*"] >> "%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!" +if not "%OPENSEARCH_JAVA_HOME%" == "" ( + set "JAVA=%OPENSEARCH_JAVA_HOME%\bin\java.exe" + set JAVA_TYPE=OPENSEARCH_JAVA_HOME +) else if not "%JAVA_HOME%" == "" ( + set "JAVA=%JAVA_HOME%\bin\java.exe" + set JAVA_TYPE=JAVA_HOME ) else ( - for /f %%a in ('type "%ADMIN_PASSWORD_FILE%"') do set "ADMIN_PASSWORD=%%a" + set "JAVA=%OPENSEARCH_HOME%\jdk\bin\java.exe" + set "JAVA_HOME=%OPENSEARCH_HOME%\jdk" + set JAVA_TYPE=bundled jdk ) -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. +if not exist "%JAVA%" ( + echo "could not find java in %JAVA_TYPE% at %JAVA%" >&2 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 -) || ( - if %cluster_mode% == 1 ( - echo network.host: 0.0.0.0 >> "%OPENSEARCH_CONF_FILE%" - echo node.name: smoketestnode >> "%OPENSEARCH_CONF_FILE%" - echo cluster.initial_cluster_manager_nodes: smoketestnode >> "%OPENSEARCH_CONF_FILE%" - ) -) - ->nul findstr /b /c:"node.max_local_storage_nodes" "%OPENSEARCH_CONF_FILE%" && ( - echo node.max_local_storage_nodes already present -) || ( - echo node.max_local_storage_nodes: 3 >> "%OPENSEARCH_CONF_FILE%" -) - -echo ######## End OpenSearch Security Demo Configuration ######## >> "%OPENSEARCH_CONF_FILE%" - -echo ### Success -echo ### Execute this script now on all your nodes and then start all nodes -:: Generate securityadmin_demo.bat -echo. > securityadmin_demo.bat -echo %OPENSEARCH_PLUGINS_DIR%opensearch-security\tools\securityadmin.bat -cd %OPENSEARCH_CONF_DIR%opensearch-security -icl -key %OPENSEARCH_CONF_DIR%kirk-key.pem -cert %OPENSEARCH_CONF_DIR%kirk.pem -cacert %OPENSEARCH_CONF_DIR%root-ca.pem -nhnv >> securityadmin_demo.bat - -if %initsecurity% == 0 ( - echo ### After the whole cluster is up execute: - type securityadmin_demo.bat - echo ### or run ./securityadmin_demo.bat - echo ### After that you can also use the Security Plugin ConfigurationGUI -) else ( - echo ### OpenSearch Security will be automatically initialized. - echo ### If you like to change the runtime configuration - echo ### change the files in ../../../config/opensearch-security and execute: - type securityadmin_demo.bat - echo ### or run ./securityadmin_demo.bat - echo ### To use the Security Plugin ConfigurationGUI -) - -echo ### To access your secured cluster open https://: and log in with admin/admin. -echo ### [Ignore the SSL certificate warning because we installed self-signed demo certificates] +"%JAVA%" -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "%DIR%\..\*;%DIR%\..\..\..\lib\*;%DIR%\..\deps\*" org.opensearch.security.tools.democonfig.Installer %DIR% %* 2> nul diff --git a/tools/install_demo_configuration.sh b/tools/install_demo_configuration.sh index 01bc1bfed1..d3a3ae8f75 100755 --- a/tools/install_demo_configuration.sh +++ b/tools/install_demo_configuration.sh @@ -1,10 +1,14 @@ #!/bin/bash #install_demo_configuration.sh [-y] -echo "**************************************************************************" -echo "** This tool will be deprecated in the next major release of OpenSearch **" -echo "** https://github.com/opensearch-project/security/issues/1755 **" -echo "**************************************************************************" +UNAME=$(uname -s) +if [ "$UNAME" = "FreeBSD" ]; then + OS="freebsd" +elif [ "$UNAME" = "Darwin" ]; then + OS="darwin" +else + OS="other" +fi SCRIPT_PATH="${BASH_SOURCE[0]}" if ! [ -x "$(command -v realpath)" ]; then @@ -21,455 +25,40 @@ else DIR="$( cd "$( dirname "$(realpath "$SCRIPT_PATH")" )" && pwd -P)" fi -echo "OpenSearch Security Demo Installer" -echo " ** Warning: Do not use on production or public reachable systems **" - -OPTIND=1 -assumeyes=0 -initsecurity=0 -cluster_mode=0 -skip_updates=-1 - -function show_help() { - echo "install_demo_configuration.sh [-y] [-i] [-c]" - echo " -h show help" - echo " -y confirm all installation dialogues automatically" - echo " -i initialize Security plugin with default configuration (default is to ask if -y is not given)" - echo " -c enable cluster mode by binding to all network interfaces (default is to ask if -y is not given)" - echo " -s skip updates if config is already applied to opensearch.yml" -} - -while getopts "h?yics" opt; do - case "$opt" in - h|\?) - show_help - exit 0 - ;; - y) assumeyes=1 - ;; - i) initsecurity=1 - ;; - c) cluster_mode=1 - ;; - s) skip_updates=0 - esac -done - -shift $((OPTIND-1)) - -[ "$1" = "--" ] && shift - -if [ "$assumeyes" == 0 ]; then - read -r -p "Install demo certificates? [y/N] " response - case "$response" in - [yY][eE][sS]|[yY]) - ;; - *) - exit 0 - ;; - esac -fi - -if [ "$initsecurity" == 0 ] && [ "$assumeyes" == 0 ]; then - read -r -p "Initialize Security Modules? [y/N] " response - case "$response" in - [yY][eE][sS]|[yY]) - initsecurity=1 - ;; - *) - initsecurity=0 - ;; - esac -fi -if [ "$cluster_mode" == 0 ] && [ "$assumeyes" == 0 ]; then - echo "Cluster mode requires maybe additional setup of:" - echo " - Virtual memory (vm.max_map_count)" - echo "" - read -r -p "Enable cluster mode? [y/N] " response - case "$response" in - [yY][eE][sS]|[yY]) - cluster_mode=1 - ;; - *) - cluster_mode=0 - ;; - esac +if [ -z "$OPENSEARCH_HOME" ]; then + # move to opensearch root folder and set the variable + OPENSEARCH_HOME=`cd "$DIR/../../.."; pwd` fi -set -e -BASE_DIR="$DIR/../../.." -if [ -d "$BASE_DIR" ]; then - CUR="$(pwd)" - cd "$BASE_DIR" - BASE_DIR="$(pwd)" - cd "$CUR" - echo "Basedir: $BASE_DIR" +# now set the path to java: OPENSEARCH_JAVA_HOME -> JAVA_HOME -> bundled JRE -> bundled JDK +if [ -n "$OPENSEARCH_JAVA_HOME" ]; then + JAVA="$OPENSEARCH_JAVA_HOME/bin/java" + JAVA_TYPE="OPENSEARCH_JAVA_HOME" +elif [ -n "$JAVA_HOME" ]; then + JAVA="$JAVA_HOME/bin/java" + JAVA_TYPE="JAVA_HOME" 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" -OPENSEARCH_MODULES_DIR="$BASE_DIR/modules" -OPENSEARCH_LIB_PATH="$BASE_DIR/lib" -SUDO_CMD="" -OPENSEARCH_INSTALL_TYPE=".tar.gz" - -#Check if its a rpm/deb install -if [ "/usr/share/opensearch" -ef "$BASE_DIR" ]; then - OPENSEARCH_CONF_FILE="/usr/share/opensearch/config/opensearch.yml" - - if [ ! -f "$OPENSEARCH_CONF_FILE" ]; then - OPENSEARCH_CONF_FILE="/etc/opensearch/opensearch.yml" - fi - - if [ -x "$(command -v sudo)" ]; then - SUDO_CMD="sudo" - echo "This script maybe require your root password for 'sudo' privileges" - fi - - OPENSEARCH_INSTALL_TYPE="rpm/deb" -fi - -if [ $SUDO_CMD ]; then - if ! [ -x "$(command -v $SUDO_CMD)" ]; then - echo "Unable to locate 'sudo' command. Quit." - exit 1 - fi -fi - -if $SUDO_CMD test -f "$OPENSEARCH_CONF_FILE"; then - : -else - echo "Unable to determine OpenSearch config directory. Quit." - exit -1 -fi - -if [ ! -d "$OPENSEARCH_BIN_DIR" ]; then - echo "Unable to determine OpenSearch bin directory. Quit." - exit -1 -fi - -if [ ! -d "$OPENSEARCH_PLUGINS_DIR" ]; then - echo "Unable to determine OpenSearch plugins directory. Quit." - exit -1 -fi - -if [ ! -d "$OPENSEARCH_MODULES_DIR" ]; then - echo "Unable to determine OpenSearch modules directory. Quit." - #exit -1 -fi - -if [ ! -d "$OPENSEARCH_LIB_PATH" ]; then - echo "Unable to determine OpenSearch lib directory. Quit." - exit -1 -fi - -OPENSEARCH_CONF_DIR=$(dirname "${OPENSEARCH_CONF_FILE}") -OPENSEARCH_CONF_DIR=`cd "$OPENSEARCH_CONF_DIR" ; pwd` - -if [ ! -d "$OPENSEARCH_PLUGINS_DIR/opensearch-security" ]; then - echo "OpenSearch Security plugin not installed. Quit." - exit -1 -fi - -OPENSEARCH_VERSION=("$OPENSEARCH_LIB_PATH/opensearch-*.jar") -OPENSEARCH_VERSION=$(echo $OPENSEARCH_VERSION | sed 's/.*opensearch-\(.*\)\.jar/\1/') - -SECURITY_VERSION=("$OPENSEARCH_PLUGINS_DIR/opensearch-security/opensearch-security-*.jar") -SECURITY_VERSION=$(echo $SECURITY_VERSION | sed 's/.*opensearch-security-\(.*\)\.jar/\1/') - -OS=$(sb_release -ds 2>/dev/null || cat /etc/*release 2>/dev/null | head -n1 || uname -om) -echo "OpenSearch install type: $OPENSEARCH_INSTALL_TYPE on $OS" -echo "OpenSearch config dir: $OPENSEARCH_CONF_DIR" -echo "OpenSearch config file: $OPENSEARCH_CONF_FILE" -echo "OpenSearch bin dir: $OPENSEARCH_BIN_DIR" -echo "OpenSearch plugins dir: $OPENSEARCH_PLUGINS_DIR" -echo "OpenSearch lib dir: $OPENSEARCH_LIB_PATH" -echo "Detected OpenSearch Version: $OPENSEARCH_VERSION" -echo "Detected OpenSearch Security Version: $SECURITY_VERSION" - -if $SUDO_CMD grep --quiet -i plugins.security "$OPENSEARCH_CONF_FILE"; then - echo "$OPENSEARCH_CONF_FILE seems to be already configured for Security. Quit." - exit $skip_updates -fi - -set +e - -read -r -d '' ADMIN_CERT << EOM ------BEGIN CERTIFICATE----- -MIIEmDCCA4CgAwIBAgIUZjrlDPP8azRDPZchA/XEsx0X2iYwDQYJKoZIhvcNAQEL -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yMzA4MjkyMDA2MzdaFw0zMzA4MjYyMDA2MzdaME0xCzAJBgNVBAYT -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs -aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC -ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs -paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+ -O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx -vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 -cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 -bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw -DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMIHPBgNVHSME -gccwgcSAFBeH36Ba62YSp9XQ+LoSRTy3KwCcoYGVpIGSMIGPMRMwEQYKCZImiZPy -LGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQRXhh -bXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290IENB -MSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0GCFHfkrz782p+T9k0G -xGeM4+BrehWKMB0GA1UdDgQWBBSjMS8tgguX/V7KSGLoGg7K6XMzIDANBgkqhkiG -9w0BAQsFAAOCAQEANMwD1JYlwAh82yG1gU3WSdh/tb6gqaSzZK7R6I0L7slaXN9m -y2ErUljpTyaHrdiBFmPhU/2Kj2r+fIUXtXdDXzizx/JdmueT0nG9hOixLqzfoC9p -fAhZxM62RgtyZoaczQN82k1/geMSwRpEndFe3OH7arkS/HSbIFxQhAIy229eWe5d -1bUzP59iu7f3r567I4ob8Vy7PP+Ov35p7Vv4oDHHwgsdRzX6pvL6mmwVrQ3BfVec -h9Dqprr+ukYmjho76g6k5cQuRaB6MxqldzUg+2E7IHQP8MCF+co51uZq2nl33mtp -RGr6JbdHXc96zsLTL3saJQ8AWEfu1gbTVrwyRA== ------END CERTIFICATE----- -EOM - -read -r -d '' ADMIN_CERT_KEY << EOM ------BEGIN PRIVATE KEY----- -MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp -gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky -AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo -7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB -GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+ -b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu -y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4 -ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0 -TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j -xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ -OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo -1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs -9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs -/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3 -qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG -/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv -M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0 -0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ -K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5 -9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF -RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp -nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5 -3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h -mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw -F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs -/AHmo368d4PSNRMMzLHw8Q== ------END PRIVATE KEY----- -EOM - -read -r -d '' NODE_CERT << EOM ------BEGIN CERTIFICATE----- -MIIEPDCCAySgAwIBAgIUZjrlDPP8azRDPZchA/XEsx0X2iIwDQYJKoZIhvcNAQEL -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yMzA4MjkwNDIzMTJaFw0zMzA4MjYwNDIzMTJaMFcxCzAJBgNVBAYT -AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl -MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA -A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud -yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 -HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr -XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n -dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD -ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R -BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA -AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF -BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo -wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ -KoZIhvcNAQELBQADggEBAD2hkndVih6TWxoe/oOW0i2Bq7ScNO/n7/yHWL04HJmR -MaHv/Xjc8zLFLgHuHaRvC02ikWIJyQf5xJt0Oqu2GVbqXH9PBGKuEP2kCsRRyU27 -zTclAzfQhqmKBTYQ/3lJ3GhRQvXIdYTe+t4aq78TCawp1nSN+vdH/1geG6QjMn5N -1FU8tovDd4x8Ib/0dv8RJx+n9gytI8n/giIaDCEbfLLpe4EkV5e5UNpOnRgJjjuy -vtZutc81TQnzBtkS9XuulovDE0qI+jQrKkKu8xgGLhgH0zxnPkKtUg2I3Aq6zl1L -zYkEOUF8Y25J6WeY88Yfnc0iigI+Pnz5NK8R9GL7TYo= ------END CERTIFICATE----- -EOM - -read -r -d '' NODE_KEY << EOM ------BEGIN PRIVATE KEY----- -MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv -bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0 -o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50 -1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1 -MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b -6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa -vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo -FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ -5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O -zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ -xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow -dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn -7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U -hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej -VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B -Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c -uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy -hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv -hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/ -A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh -KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX -GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f -5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud -tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71 -+x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT -bg/ch9Rhxbq22yrVgWHh6epp ------END PRIVATE KEY----- -EOM - -read -r -d '' ROOT_CA << EOM ------BEGIN CERTIFICATE----- -MIIExjCCA66gAwIBAgIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcNAQEL -BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt -cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl -IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v -dCBDQTAeFw0yMzA4MjkwNDIwMDNaFw0yMzA5MjgwNDIwMDNaMIGPMRMwEQYKCZIm -iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ -RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 -IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU -j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4 -U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg -vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA -WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969 -VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW -MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU -F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 -uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ -k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD -VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg -Q29tIEluYy4gUm9vdCBDQYIUd+SvPvzan5P2TQbEZ4zj4Gt6FYowDQYJKoZIhvcN -AQELBQADggEBAIopqco/k9RSjouTeKP4z0EVUxdD4qnNh1GLSRqyAVe0aChyKF5f -qt1Bd1XCY8D16RgekkKGHDpJhGCpel+vtIoXPBxUaGQNYxmJCf5OzLMODlcrZk5i -jHIcv/FMeK02NBcz/WQ3mbWHVwXLhmwqa2zBsF4FmPCJAbFLchLhkAv1HJifHbnD -jQzlKyl5jxam/wtjWxSm0iyso0z2TgyzY+MESqjEqB1hZkCFzD1xtUOCxbXgtKae -dgfHVFuovr3fNLV3GvQk0s9okDwDUcqV7DSH61e5bUMfE84o3of8YA7+HUoPV5Du -8sTOKRf7ncGXdDRA8aofW268pTCuIu3+g/Y= ------END CERTIFICATE----- -EOM - -set -e - -echo "$ADMIN_CERT" | $SUDO_CMD tee "$OPENSEARCH_CONF_DIR/kirk.pem" > /dev/null -echo "$NODE_CERT" | $SUDO_CMD tee "$OPENSEARCH_CONF_DIR/esnode.pem" > /dev/null -echo "$ROOT_CA" | $SUDO_CMD tee "$OPENSEARCH_CONF_DIR/root-ca.pem" > /dev/null -echo "$NODE_KEY" | $SUDO_CMD tee "$OPENSEARCH_CONF_DIR/esnode-key.pem" > /dev/null -echo "$ADMIN_CERT_KEY" | $SUDO_CMD tee "$OPENSEARCH_CONF_DIR/kirk-key.pem" > /dev/null - -chmod 0600 "$OPENSEARCH_CONF_DIR/kirk.pem" -chmod 0600 "$OPENSEARCH_CONF_DIR/esnode.pem" -chmod 0600 "$OPENSEARCH_CONF_DIR/root-ca.pem" -chmod 0600 "$OPENSEARCH_CONF_DIR/esnode-key.pem" -chmod 0600 "$OPENSEARCH_CONF_DIR/kirk-key.pem" - -echo "" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" -echo "######## Start OpenSearch Security Demo Configuration ########" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "# WARNING: revise all the lines below before you go into production" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.transport.pemcert_filepath: esnode.pem" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.transport.pemkey_filepath: esnode-key.pem" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.transport.pemtrustedcas_filepath: root-ca.pem" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.transport.enforce_hostname_verification: false" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.http.enabled: true" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.http.pemcert_filepath: esnode.pem" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.http.pemkey_filepath: esnode-key.pem" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.ssl.http.pemtrustedcas_filepath: root-ca.pem" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.allow_unsafe_democertificates: true" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -if [ "$initsecurity" == 1 ]; then - echo "plugins.security.allow_default_init_securityindex: true" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -fi -echo "plugins.security.authcz.admin_dn:" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo " - CN=kirk,OU=client,O=client,L=test, C=de" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.audit.type: internal_opensearch" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -echo "plugins.security.enable_snapshot_restore_privilege: true" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -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*"]' | $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 -else - if [ "$cluster_mode" == 1 ]; then - echo "network.host: 0.0.0.0" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null - echo "node.name: smoketestnode" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null - echo "cluster.initial_cluster_manager_nodes: smoketestnode" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null - fi -fi - -if $SUDO_CMD grep --quiet -i "^node.max_local_storage_nodes" "$OPENSEARCH_CONF_FILE"; then - : #already present -else - echo 'node.max_local_storage_nodes: 3' | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null -fi - - - -echo "######## End OpenSearch Security Demo Configuration ########" | $SUDO_CMD tee -a "$OPENSEARCH_CONF_FILE" > /dev/null - -$SUDO_CMD chmod +x "$OPENSEARCH_PLUGINS_DIR/opensearch-security/tools/securityadmin.sh" - -OPENSEARCH_PLUGINS_DIR=`cd "$OPENSEARCH_PLUGINS_DIR" ; pwd` - -echo "### Success" -echo "### Execute this script now on all your nodes and then start all nodes" -#Generate securityadmin_demo.sh -echo "#!/bin/bash" | $SUDO_CMD tee securityadmin_demo.sh > /dev/null -echo $SUDO_CMD \""$OPENSEARCH_PLUGINS_DIR/opensearch-security/tools/securityadmin.sh"\" -cd \""$OPENSEARCH_CONF_DIR/opensearch-security"\" -icl -key \""$OPENSEARCH_CONF_DIR/kirk-key.pem"\" -cert \""$OPENSEARCH_CONF_DIR/kirk.pem"\" -cacert \""$OPENSEARCH_CONF_DIR/root-ca.pem"\" -nhnv | $SUDO_CMD tee -a securityadmin_demo.sh > /dev/null -$SUDO_CMD chmod +x securityadmin_demo.sh - -if [ "$initsecurity" == 0 ]; then - echo "### After the whole cluster is up execute: " - $SUDO_CMD cat securityadmin_demo.sh | tail -1 - echo "### or run ./securityadmin_demo.sh" - echo "### After that you can also use the Security Plugin ConfigurationGUI" -else - echo "### OpenSearch Security will be automatically initialized." - echo "### If you like to change the runtime configuration " - echo "### change the files in ../../../config/opensearch-security and execute: " - $SUDO_CMD cat securityadmin_demo.sh | tail -1 - echo "### or run ./securityadmin_demo.sh" - echo "### To use the Security Plugin ConfigurationGUI" -fi - -echo "### To access your secured cluster open https://: and log in with admin/admin." -echo "### (Ignore the SSL certificate warning because we installed self-signed demo certificates)" + if [ "$OS" = "darwin" ]; then + # macOS bundled Java + JAVA="$OPENSEARCH_HOME/jdk.app/Contents/Home/bin/java" + JAVA_TYPE="bundled jdk" + elif [ "$OS" = "freebsd" ]; then + # using FreeBSD default java from ports if JAVA_HOME is not set + JAVA="/usr/local/bin/java" + JAVA_TYPE="bundled jdk" + elif [ -d "$OPENSEARCH_HOME/jre" ]; then + JAVA="$OPENSEARCH_HOME/jre/bin/java" + JAVA_TYPE="bundled jre" + else + JAVA="$OPENSEARCH_HOME/jdk/bin/java" + JAVA_TYPE="bundled jdk" + fi +fi + +if [ ! -x "$JAVA" ]; then + echo "could not find java in $JAVA_TYPE at $JAVA" >&2 + exit 1 +fi + +"$JAVA" -Dorg.apache.logging.log4j.simplelog.StatusLogger.level=OFF -cp "$DIR/../*:$DIR/../../../lib/*:$DIR/../deps/*" org.opensearch.security.tools.democonfig.Installer "$DIR" "$@" 2>/dev/null