diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 362bb78d69..bdf34afd76 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,9 +37,35 @@ jobs: run: | echo "separateTestsNames=$(./gradlew listTasksAsJSON -q --console=plain | tail -n 1)" >> $GITHUB_OUTPUT + publish-components-to-maven-local: + runs-on: ubuntu-latest + steps: + - name: Set up JDK for build and test + uses: actions/setup-java@v4 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: 21 + + - name: Checkout security + uses: actions/checkout@v4 + + - name: Publish components to Maven Local + run: | + ./gradlew clean \ + :opensearch-resource-sharing-spi:publishToMavenLocal \ + -Dbuild.snapshot=false + + - name: Cache artifacts for dependent jobs + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository/org/opensearch/ + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + test-windows: name: test - needs: generate-test-list + needs: [generate-test-list, publish-components-to-maven-local] strategy: fail-fast: false matrix: @@ -101,6 +127,14 @@ jobs: - name: Checkout security uses: actions/checkout@v4 + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository/org/opensearch/ + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + - name: Build and Test uses: gradle/gradle-build-action@v3 with: @@ -116,7 +150,7 @@ jobs: ./build/reports/ report-coverage: - needs: ["test-windows", "test-linux", "integration-tests-windows", "integration-tests-linux"] + needs: ["test-windows", "test-linux", "integration-tests-windows", "integration-tests-linux", "spi-tests-linux", "spi-tests-windows"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -139,7 +173,6 @@ jobs: fail_ci_if_error: true verbose: true - integration-tests-windows: name: integration-tests strategy: @@ -159,12 +192,20 @@ jobs: - name: Checkout security uses: actions/checkout@v4 - - name: Build and Test + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository/org/opensearch/ + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + + - name: Run Integration Tests uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: | - integrationTest -Dbuild.snapshot=false + :integrationTest -Dbuild.snapshot=false - uses: actions/upload-artifact@v4 if: always() @@ -192,6 +233,7 @@ jobs: steps: - name: Run start commands run: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-start-command }} + - name: Set up JDK for build and test uses: actions/setup-java@v4 with: @@ -215,9 +257,103 @@ jobs: path: | ./build/reports/ + spi-tests-linux: + name: spi-tests + needs: ["Get-CI-Image-Tag", publish-components-to-maven-local] + strategy: + fail-fast: false + matrix: + jdk: [21] + platform: [ubuntu-latest] + runs-on: ${{ matrix.platform }} + container: + # using the same image which is used by opensearch-build to build the OpenSearch Distribution + # this image tag is subject to change as more dependencies and updates will arrive over time + image: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-version-linux }} + # need to switch to root so that github actions can install runner binary on container without permission issues. + options: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-start-options }} + + steps: + - name: Run start commands + run: ${{ needs.Get-CI-Image-Tag.outputs.ci-image-start-command }} + + - name: Set up JDK for build and test + uses: actions/setup-java@v4 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: ${{ matrix.jdk }} + + - name: Checkout security + uses: actions/checkout@v4 + + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository/org/opensearch/ + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + + - name: Run SPI Tests + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: | + :opensearch-resource-sharing-spi:test -Dbuild.snapshot=false + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: spi-${{ matrix.platform }}-JDK${{ matrix.jdk }}-reports + path: | + ./build/reports/ + + spi-tests-windows: + name: spi-tests + needs: publish-components-to-maven-local + strategy: + fail-fast: false + matrix: + jdk: [21] + platform: [windows-latest] + runs-on: ${{ matrix.platform }} + + steps: + - name: Set up JDK for build and test + uses: actions/setup-java@v4 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: ${{ matrix.jdk }} + + - name: Checkout security + uses: actions/checkout@v4 + + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository/org/opensearch/ + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + + - name: Run SPI Tests + uses: gradle/gradle-build-action@v3 + with: + cache-disabled: true + arguments: | + :opensearch-resource-sharing-spi:test -Dbuild.snapshot=false + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: spi-${{ matrix.platform }}-JDK${{ matrix.jdk }}-reports + path: | + ./build/reports/ + resource-tests: env: CI_ENVIRONMENT: resource-test + needs: publish-components-to-maven-local strategy: fail-fast: false matrix: @@ -235,12 +371,20 @@ jobs: - name: Checkout security uses: actions/checkout@v4 - - name: Build and Test + - name: Restore Maven Local Cache + uses: actions/cache@v4.2.2 + with: + path: ~/.m2/repository/org/opensearch/ + key: maven-local-${{ github.run_id }} + restore-keys: | + maven-local- + + - name: Run Resource Tests uses: gradle/gradle-build-action@v3 with: cache-disabled: true arguments: | - integrationTest -Dbuild.snapshot=false --tests org.opensearch.security.ResourceFocusedTests + :integrationTest -Dbuild.snapshot=false --tests org.opensearch.security.ResourceFocusedTests backward-compatibility-build: runs-on: ubuntu-latest @@ -303,40 +447,62 @@ jobs: build-artifact-names: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Setup Environment + uses: actions/checkout@v4 - - uses: actions/setup-java@v4 + - name: Configure Java + uses: actions/setup-java@v4 with: - distribution: temurin # Temurin is a distribution of adoptium + distribution: temurin java-version: 21 - - run: | - security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') - security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') - security_plugin_version_only_number=$(echo $security_plugin_version_no_snapshot | cut -d- -f1) - test_qualifier=alpha2 - - echo "SECURITY_PLUGIN_VERSION=$security_plugin_version" >> $GITHUB_ENV - echo "SECURITY_PLUGIN_VERSION_NO_SNAPSHOT=$security_plugin_version_no_snapshot" >> $GITHUB_ENV - echo "SECURITY_PLUGIN_VERSION_ONLY_NUMBER=$security_plugin_version_only_number" >> $GITHUB_ENV - echo "TEST_QUALIFIER=$test_qualifier" >> $GITHUB_ENV - - - run: | - echo ${{ env.SECURITY_PLUGIN_VERSION }} - echo ${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }} - echo ${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }} - echo ${{ env.TEST_QUALIFIER }} - - - run: ./gradlew clean assemble && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION }}.zip - - - run: ./gradlew clean assemble -Dbuild.snapshot=false && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_NO_SNAPSHOT }}.zip - - - run: ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=${{ env.TEST_QUALIFIER }} && test -s ./build/distributions/opensearch-security-${{ env.SECURITY_PLUGIN_VERSION_ONLY_NUMBER }}-${{ env.TEST_QUALIFIER }}.zip - - - 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/ + - name: Build and Test Artifacts + run: | + # Set version variables + security_plugin_version=$(./gradlew properties -q | grep -E '^version:' | awk '{print $2}') + security_plugin_version_no_snapshot=$(echo $security_plugin_version | sed 's/-SNAPSHOT//g') + security_plugin_version_only_number=$(echo $security_plugin_version_no_snapshot | cut -d- -f1) + test_qualifier=alpha2 + + # Debug print versions + echo "Versions:" + echo $security_plugin_version + echo $security_plugin_version_no_snapshot + echo $security_plugin_version_only_number + echo $test_qualifier + + # Publish SPI + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot-all.jar + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-all.jar + ./gradlew clean :opensearch-resource-sharing-spi:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT-all.jar + + + # Build artifacts + ./gradlew clean assemble && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar + + ./gradlew clean assemble -Dbuild.snapshot=false && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version_no_snapshot.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar + + ./gradlew clean assemble -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar + + ./gradlew clean assemble -Dbuild.version_qualifier=$test_qualifier && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.zip && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar + + ./gradlew clean publishPluginZipPublicationToZipStagingRepository && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version.zip && \ + test -s ./build/distributions/opensearch-security-$security_plugin_version.pom && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar + + ./gradlew clean publishShadowPublicationToMavenLocal && \ + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar + + - name: List files in build directory on failure if: failure() + run: ls -al ./*/build/libs/ ./build/distributions/ diff --git a/.github/workflows/maven-publish.yml b/.github/workflows/maven-publish.yml index d10fd67beb..42d07fbb0a 100644 --- a/.github/workflows/maven-publish.yml +++ b/.github/workflows/maven-publish.yml @@ -32,4 +32,4 @@ jobs: export SONATYPE_PASSWORD=$(aws secretsmanager get-secret-value --secret-id maven-snapshots-password --query SecretString --output text) echo "::add-mask::$SONATYPE_USERNAME" echo "::add-mask::$SONATYPE_PASSWORD" - ./gradlew publishPluginZipPublicationToSnapshotsRepository + ./gradlew --no-daemon publishPluginZipPublicationToSnapshotsRepository publishShadowPublicationToSnapshotsRepository diff --git a/.gitignore b/.gitignore index 6fbfafabac..5eb2da999f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,3 @@ out/ build/ gradle-build/ .gradle/ - -# nodejs -node_modules/ -package-lock.json diff --git a/build.gradle b/build.gradle index c5a6d9d176..2113060e39 100644 --- a/build.gradle +++ b/build.gradle @@ -500,6 +500,12 @@ configurations { force "org.checkerframework:checker-qual:3.49.1" force "ch.qos.logback:logback-classic:1.5.17" force "commons-io:commons-io:2.18.0" + force "com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2" + force "org.hamcrest:hamcrest:2.2" + force "org.mockito:mockito-core:5.16.1" + force "net.bytebuddy:byte-buddy:1.15.11" + force "org.ow2.asm:asm:9.7.1" + force "com.google.j2objc:j2objc-annotations:3.0.0" } } @@ -507,6 +513,65 @@ configurations { integrationTestRuntimeOnly.extendsFrom runtimeOnly } +allprojects { + configurations { + integrationTestImplementation.extendsFrom implementation + compile.extendsFrom compileOnly + compile.extendsFrom testImplementation + } + dependencies { + // unit test framework + testImplementation 'org.hamcrest:hamcrest:2.2' + testImplementation 'junit:junit:4.13.2' + testImplementation "org.opensearch:opensearch:${opensearch_version}" + testImplementation "org.mockito:mockito-core:5.16.1" + + //integration test framework: + integrationTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2') { + exclude(group: 'junit', module: 'junit') + } + integrationTestImplementation 'junit:junit:4.13.2' + integrationTestImplementation("org.opensearch.plugin:reindex-client:${opensearch_version}"){ + exclude(group: 'org.slf4j', module: 'slf4j-api') + } + integrationTestImplementation "org.opensearch.plugin:percolator-client:${opensearch_version}" + integrationTestImplementation 'commons-io:commons-io:2.18.0' + integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" + integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}" + integrationTestImplementation 'org.hamcrest:hamcrest:2.2' + integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" + integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}" + integrationTestImplementation('org.awaitility:awaitility:4.2.2') { + exclude(group: 'org.hamcrest', module: 'hamcrest') + } + integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14' + integrationTestImplementation "org.opensearch.plugin:mapper-size:${opensearch_version}" + integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14" + integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14" + integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14" + integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16" + integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" + integrationTestImplementation "org.mockito:mockito-core:5.16.1" + integrationTestImplementation "org.passay:passay:1.6.6" + integrationTestImplementation "org.opensearch:opensearch:${opensearch_version}" + integrationTestImplementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" + integrationTestImplementation "org.opensearch.plugin:aggs-matrix-stats-client:${opensearch_version}" + integrationTestImplementation "org.opensearch.plugin:parent-join-client:${opensearch_version}" + integrationTestImplementation 'com.password4j:password4j:1.8.2' + integrationTestImplementation "com.google.guava:guava:${guava_version}" + integrationTestImplementation "org.apache.commons:commons-lang3:${versions.commonslang}" + integrationTestImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + integrationTestImplementation 'org.greenrobot:eventbus-java:3.3.1' + integrationTestImplementation('com.flipkart.zjsonpatch:zjsonpatch:0.4.16'){ + exclude(group:'com.fasterxml.jackson.core') + } + integrationTestImplementation 'org.slf4j:slf4j-api:2.0.12' + integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0' + integrationTestImplementation "org.opensearch.plugin:lang-painless:${opensearch_version}" + integrationTestImplementation project(path:":opensearch-resource-sharing-spi", configuration: 'shadow') + } +} + //create source set 'integrationTest' //add classes from the main source set to the compilation and runtime classpaths of the integrationTest sourceSets { @@ -575,6 +640,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { + implementation project(path:":opensearch-resource-sharing-spi", configuration: 'shadow') implementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" implementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" implementation "org.apache.httpcomponents.client5:httpclient5-cache:${versions.httpclient5}" @@ -730,35 +796,11 @@ dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" - //integration test framework: - integrationTestImplementation('com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.2') { - exclude(group: 'junit', module: 'junit') - } - 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.18.0' - integrationTestImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" - integrationTestImplementation "org.apache.logging.log4j:log4j-jul:${versions.log4j}" - integrationTestImplementation 'org.hamcrest:hamcrest:2.2' - integrationTestImplementation "org.bouncycastle:bcpkix-jdk18on:${versions.bouncycastle}" - integrationTestImplementation "org.bouncycastle:bcutil-jdk18on:${versions.bouncycastle}" - integrationTestImplementation('org.awaitility:awaitility:4.3.0') { - exclude(group: 'org.hamcrest', module: 'hamcrest') - } - integrationTestImplementation 'com.unboundid:unboundid-ldapsdk:4.0.14' - integrationTestImplementation "org.opensearch.plugin:mapper-size:${opensearch_version}" - integrationTestImplementation "org.apache.httpcomponents:httpclient-cache:4.5.14" - integrationTestImplementation "org.apache.httpcomponents:httpclient:4.5.14" - integrationTestImplementation "org.apache.httpcomponents:fluent-hc:4.5.14" - integrationTestImplementation "org.apache.httpcomponents:httpcore:4.4.16" - integrationTestImplementation "org.apache.httpcomponents:httpasyncclient:4.1.5" - integrationTestImplementation "org.mockito:mockito-core:5.16.1" - //spotless implementation('com.google.googlejavaformat:google-java-format:1.25.2') { exclude group: 'com.google.guava' } + } jar { diff --git a/scripts/build.sh b/scripts/build.sh index 4b2893f304..c4476731f5 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -77,6 +77,10 @@ echo "COPY ${distributions}/*.zip" mkdir -p $OUTPUT/plugins cp ${distributions}/*.zip ./$OUTPUT/plugins +# Publish jars +./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER +./gradlew publishAllPublicationsToStagingRepository -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER + ./gradlew publishPluginZipPublicationToZipStagingRepository -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER mkdir -p $OUTPUT/maven/org/opensearch cp -r ./build/local-staging-repo/org/opensearch/. $OUTPUT/maven/org/opensearch diff --git a/settings.gradle b/settings.gradle index 1c3e7ff5aa..193587dee7 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,6 @@ */ rootProject.name = 'opensearch-security' + +include "spi" +project(":spi").name = "opensearch-resource-sharing-spi" diff --git a/spi/README.md b/spi/README.md new file mode 100644 index 0000000000..1a715ed180 --- /dev/null +++ b/spi/README.md @@ -0,0 +1,168 @@ +# **Resource Sharing and Access Control SPI** + +This **Service Provider Interface (SPI)** provides the necessary **interfaces and mechanisms** to implement **Resource Sharing and Access Control** in OpenSearch. + +--- + +## **Usage** + +A plugin that **defines a resource** and aims to implement **access control** over that resource must **extend** the `ResourceSharingExtension` class to register itself as a **Resource Plugin**. + +### **Example: Implementing a Resource Plugin** +```java +public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, ResourceSharingExtension { + + // Override required methods + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = + new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Sample index with resources"); + return Collections.singletonList(systemIndexDescriptor); + } + + @Override + public String getResourceType() { + return SampleResource.class.getCanonicalName(); + } + + @Override + public String getResourceIndex() { + return RESOURCE_INDEX_NAME; + } + + @Override + public ResourceParser getResourceParser() { + return new SampleResourceParser(); + } +} +``` + +--- + +## **Checklist for Implementing a Resource Plugin** + +To properly integrate with the **Resource Sharing and Access Control SPI**, follow these steps: + +### **1. Add Required Dependencies** +Include **`opensearch-security-client`** and **`opensearch-resource-sharing-spi`** in your **`build.gradle`** file. +Example: +```gradle +dependencies { + implementation 'org.opensearch:opensearch-security-client:VERSION' + implementation 'org.opensearch:opensearch-resource-sharing-spi:VERSION' +} +``` + +--- + +### **2. Register the Plugin Using the Java SPI Mechanism** +- Navigate to your plugin's `src/main/resources` folder. +- Locate or create the `META-INF/services` directory. +- Inside `META-INF/services`, create a file named: + ``` + org.opensearch.security.spi.resources.ResourceSharingExtension + ``` +- Edit the file and add a **single line** containing the **fully qualified class name** of your plugin implementation. + Example: + ``` + org.opensearch.sample.SampleResourcePlugin + ``` + > This step ensures that OpenSearch **dynamically loads your plugin** as a resource-sharing extension. + +--- + +### **3. Declare a Resource Class** +Each plugin must define a **resource class** that implements the `Resource` interface. +Example: +```java +public class SampleResource implements Resource { + private String id; + private String owner; + + // Constructor, getters, setters, etc. + + @Override + public String getResourceId() { + return id; + } +} +``` + +--- + +### **4. Implement a Resource Parser** +A **`ResourceParser`** is required to convert **resource data** from OpenSearch indices. +Example: +```java +public class SampleResourceParser implements ResourceParser { + @Override + public SampleResource parseXContent(XContentParser parser) throws IOException { + return SampleResource.fromXContent(parser); + } +} +``` + +--- + +### **5. Implement the `ResourceSharingExtension` Interface** +Ensure that your **plugin declaration class** implements `ResourceSharingExtension` and provides **all required methods**. + +**Important:** Mark the resource **index as a system index** to enforce security protections. + +--- + +### **6. Create a Client Accessor** +A **singleton accessor** should be created to manage the `ResourceSharingNodeClient`. +Example: +```java +public class ResourceSharingClientAccessor { + private static ResourceSharingNodeClient INSTANCE; + + private ResourceSharingClientAccessor() {} + + public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) { + if (INSTANCE == null) { + INSTANCE = new ResourceSharingNodeClient(nodeClient, settings); + } + return INSTANCE; + } +} +``` + +--- + +### **7. Utilize `ResourceSharingNodeClient` for Access Control** +Use the **client API methods** to manage resource sharing. + +#### **Example: Verifying Resource Access** +```java +Set scopes = Set.of("read_only"); +ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); +resourceSharingClient.verifyResourceAccess( + "resource-123", + "resource_index", + scopes, + ActionListener.wrap(isAuthorized -> { + if (isAuthorized) { + System.out.println("User has access to the resource."); + } else { + System.out.println("Access denied."); + } + }, e -> { + System.err.println("Failed to verify access: " + e.getMessage()); + }) +); +``` + +--- + +## **License** +This project is licensed under the **Apache 2.0 License**. + +--- + +## **Copyright** +© OpenSearch Contributors. + +--- diff --git a/spi/build.gradle b/spi/build.gradle new file mode 100644 index 0000000000..b8f33319b3 --- /dev/null +++ b/spi/build.gradle @@ -0,0 +1,86 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'java' + id 'maven-publish' + id 'io.github.goooler.shadow' version "8.1.7" +} + +ext { + opensearch_version = System.getProperty("opensearch.version", "3.0.0-alpha1-SNAPSHOT") +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { + compileOnly "org.opensearch:opensearch:${opensearch_version}" +} + +java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +task sourcesJar(type: Jar) { + archiveClassifier.set 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + archiveClassifier.set 'javadoc' + from tasks.javadoc +} + +publishing { + publications { + shadow(MavenPublication) { publication -> + project.shadow.component(publication) + artifact sourcesJar + artifact javadocJar + pom { + name.set("OpenSearch Resource Sharing SPI") + packaging = "jar" + description.set("OpenSearch Security Resource Sharing") + url.set("https://github.com/opensearch-project/security") + licenses { + license { + name.set("The Apache License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + } + } + scm { + connection.set("scm:git@github.com:opensearch-project/security.git") + developerConnection.set("scm:git@github.com:opensearch-project/security.git") + url.set("https://github.com/opensearch-project/security.git") + } + developers { + developer { + name.set("OpenSearch Contributors") + url.set("https://github.com/opensearch-project") + } + } + } + } + } + repositories { + maven { + name = "Snapshots" + url = "https://aws.oss.sonatype.org/content/repositories/snapshots" + credentials { + username "$System.env.SONATYPE_USERNAME" + password "$System.env.SONATYPE_PASSWORD" + } + } + maven { + name = 'staging' + url = "${rootProject.buildDir}/local-staging-repo" + } + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java new file mode 100644 index 0000000000..6521ce7d90 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java @@ -0,0 +1,38 @@ +/* + * 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.spi.resources; + +import java.util.Arrays; + +/** + * This interface defines the two basic access scopes for resource-access. Plugins can decide whether to use these. + * Each plugin must implement their own scopes and manage them. + * These access scopes will then be used to verify the type of access being requested. + * + * @opensearch.experimental + */ +public interface ResourceAccessScope> { + String READ_ONLY = "read_only"; + String PUBLIC = "public"; // users: ["*"], roles: ["*"], backend_roles: ["*"] + + static & ResourceAccessScope> E fromValue(Class enumClass, String value) { + for (E enumConstant : enumClass.getEnumConstants()) { + if (enumConstant.value().equalsIgnoreCase(value)) { + return enumConstant; + } + } + throw new IllegalArgumentException("Unknown value: " + value); + } + + String value(); + + static & ResourceAccessScope> String[] values(Class enumClass) { + return Arrays.stream(enumClass.getEnumConstants()).map(ResourceAccessScope::value).toArray(String[]::new); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java new file mode 100644 index 0000000000..47aab11136 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.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. + */ + +package org.opensearch.security.spi.resources; + +/** + * This interface should be implemented by all the plugins that define one or more resources and need access control over those resources. + * + * @opensearch.experimental + */ +public interface ResourceSharingExtension { + + /** + * Type of the resource + * @return a string containing the type of the resource. A qualified class name can be supplied here. + */ + String getResourceType(); + + /** + * The index where resource is stored + * @return the name of the parent index where resource is stored + */ + String getResourceIndex(); + + /** + * The parser for the resource, which will be used by security plugin to parse the resource + * @return the parser for the resource + */ + ShareableResourceParser getShareableResourceParser(); +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ShareableResource.java b/spi/src/main/java/org/opensearch/security/spi/resources/ShareableResource.java new file mode 100644 index 0000000000..aa22a72337 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ShareableResource.java @@ -0,0 +1,27 @@ +/* + * 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.spi.resources; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.xcontent.ToXContentFragment; + +/** + * Marker interface for all shareable resources + * + * @opensearch.experimental + */ +public interface ShareableResource extends NamedWriteable, ToXContentFragment { + /** + * Abstract method to get the resource name. + * Must be implemented by plugins defining resources. + * + * @return the resource name + */ + String getName(); +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/ShareableResourceParser.java b/spi/src/main/java/org/opensearch/security/spi/resources/ShareableResourceParser.java new file mode 100644 index 0000000000..4128c427d4 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ShareableResourceParser.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. + */ + +package org.opensearch.security.spi.resources; + +import java.io.IOException; + +import org.opensearch.core.xcontent.XContentParser; + +/** + * Interface for parsing shareable resources from XContentParser + * @param the type of resource to be parsed + * + * @opensearch.experimental + */ +public interface ShareableResourceParser { + /** + * Parse source bytes supplied by the parser to a desired ShareableResource type + * @param parser to parser bytes-ref json input + * @return the parsed object of ShareableResource type + * @throws IOException if something went wrong while parsing + */ + T parseXContent(XContentParser parser) throws IOException; +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceNonSystemIndexException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceNonSystemIndexException.java new file mode 100644 index 0000000000..fb8d5af2c8 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceNonSystemIndexException.java @@ -0,0 +1,46 @@ +/* + * 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.spi.resources.exceptions; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.rest.RestStatus; + +/** + * This class represents an exception that is caused when a resource-index is registered as non-system index. + * It extends the ResourceSharingException class. + * + * @opensearch.experimental + */ +public final class ResourceNonSystemIndexException extends ResourceSharingException { + public ResourceNonSystemIndexException(Throwable cause) { + super(cause); + } + + public ResourceNonSystemIndexException(String msg, Object... args) { + super(msg, args); + } + + public ResourceNonSystemIndexException(String msg, Throwable cause, Object... args) { + super(msg, cause, args); + } + + public ResourceNonSystemIndexException(StreamInput in) throws IOException { + super(in); + } + + @Override + public RestStatus status() { + return RestStatus.BAD_REQUEST; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceNotFoundException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceNotFoundException.java new file mode 100644 index 0000000000..67ffb8a411 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceNotFoundException.java @@ -0,0 +1,46 @@ +/* + * 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.spi.resources.exceptions; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.rest.RestStatus; + +/** + * This class represents an exception that occurs when resource sharing feature is disabled or security plugin is disabled. + * It extends the ResourceSharingException class. + * + * @opensearch.experimental + */ +public final class ResourceNotFoundException extends ResourceSharingException { + public ResourceNotFoundException(Throwable cause) { + super(cause); + } + + public ResourceNotFoundException(String msg, Object... args) { + super(msg, args); + } + + public ResourceNotFoundException(String msg, Throwable cause, Object... args) { + super(msg, cause, args); + } + + public ResourceNotFoundException(StreamInput in) throws IOException { + super(in); + } + + @Override + public RestStatus status() { + return RestStatus.NOT_FOUND; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java new file mode 100644 index 0000000000..46d583e1e2 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java @@ -0,0 +1,47 @@ +/* + * 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.spi.resources.exceptions; + +import java.io.IOException; + +import org.opensearch.OpenSearchException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.rest.RestStatus; + +/** + * This class represents a generic exception that occurs during resource sharing operations. + * It extends the OpenSearchException class. + * + * @opensearch.experimental + */ +public class ResourceSharingException extends OpenSearchException { + public ResourceSharingException(Throwable cause) { + super(cause); + } + + public ResourceSharingException(String msg, Object... args) { + super(msg, args); + } + + public ResourceSharingException(String msg, Throwable cause, Object... args) { + super(msg, cause, args); + } + + public ResourceSharingException(StreamInput in) throws IOException { + super(in); + } + + @Override + public RestStatus status() { + return RestStatus.INTERNAL_SERVER_ERROR; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingFeatureDisabledException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingFeatureDisabledException.java new file mode 100644 index 0000000000..6a7dcc8291 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingFeatureDisabledException.java @@ -0,0 +1,60 @@ +/* + * 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.spi.resources.exceptions; + +import java.io.IOException; + +import org.opensearch.OpenSearchException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.rest.RestStatus; + +/** + * This class represents an exception that occurs during resource sharing operations. + * It extends the OpenSearchException class. + * + * @opensearch.experimental + */ +public final class ResourceSharingFeatureDisabledException extends OpenSearchException { + public ResourceSharingFeatureDisabledException(Throwable cause) { + super(cause); + } + + public ResourceSharingFeatureDisabledException(String msg, Object... args) { + super(msg, args); + } + + public ResourceSharingFeatureDisabledException(String msg, Throwable cause, Object... args) { + super(msg, cause, args); + } + + public ResourceSharingFeatureDisabledException(StreamInput in) throws IOException { + super(in); + } + + @Override + public RestStatus status() { + String message = getMessage(); + if (message.contains("not authorized")) { + return RestStatus.FORBIDDEN; + } else if (message.startsWith("No authenticated")) { + return RestStatus.UNAUTHORIZED; + } else if (message.contains("not found")) { + return RestStatus.NOT_FOUND; + } else if (message.contains("not a system index")) { + return RestStatus.BAD_REQUEST; + } else if (message.contains("is disabled")) { + return RestStatus.NOT_IMPLEMENTED; + } + + return RestStatus.INTERNAL_SERVER_ERROR; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/UnauthenticatedResourceAccessException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/UnauthenticatedResourceAccessException.java new file mode 100644 index 0000000000..634a910b21 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/UnauthenticatedResourceAccessException.java @@ -0,0 +1,46 @@ +/* + * 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.spi.resources.exceptions; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.rest.RestStatus; + +/** + * This class represents an exception that occurs when an unauthenticated user tries to access a resource. + * It extends the ResourceSharingException class. + * + * @opensearch.experimental + */ +public final class UnauthenticatedResourceAccessException extends ResourceSharingException { + public UnauthenticatedResourceAccessException(Throwable cause) { + super(cause); + } + + public UnauthenticatedResourceAccessException(String msg, Object... args) { + super(msg, args); + } + + public UnauthenticatedResourceAccessException(String msg, Throwable cause, Object... args) { + super(msg, cause, args); + } + + public UnauthenticatedResourceAccessException(StreamInput in) throws IOException { + super(in); + } + + @Override + public RestStatus status() { + return RestStatus.UNAUTHORIZED; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/UnauthorizedResourceAccessException.java b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/UnauthorizedResourceAccessException.java new file mode 100644 index 0000000000..888a0af7d7 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/UnauthorizedResourceAccessException.java @@ -0,0 +1,46 @@ +/* + * 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.spi.resources.exceptions; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.rest.RestStatus; + +/** + * This class represents an exception that occurs when an unauthorized user tries to access the resource. + * It extends the ResourceSharingException class. + * + * @opensearch.experimental + */ +public final class UnauthorizedResourceAccessException extends ResourceSharingException { + public UnauthorizedResourceAccessException(Throwable cause) { + super(cause); + } + + public UnauthorizedResourceAccessException(String msg, Object... args) { + super(msg, args); + } + + public UnauthorizedResourceAccessException(String msg, Throwable cause, Object... args) { + super(msg, cause, args); + } + + public UnauthorizedResourceAccessException(StreamInput in) throws IOException { + super(in); + } + + @Override + public RestStatus status() { + return RestStatus.FORBIDDEN; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java b/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java new file mode 100644 index 0000000000..f2e210a5e5 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java @@ -0,0 +1,15 @@ +/* + * 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. + */ + +/** + * This package defines classes required to implement resource access control in OpenSearch. + * This package will be added as a dependency by all OpenSearch plugins that require resource access control. + * + * @opensearch.experimental + */ +package org.opensearch.security.spi.resources; diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java new file mode 100644 index 0000000000..50bdd1aea7 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java @@ -0,0 +1,88 @@ +/* + * 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.spi.resources.sharing; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class is used to store information about the creator of a resource. + * + * @opensearch.experimental + */ +public class CreatedBy implements ToXContentFragment, NamedWriteable { + + private final Creator creatorType; + private final String creator; + + public CreatedBy(Creator creatorType, String creator) { + this.creatorType = creatorType; + this.creator = creator; + } + + public CreatedBy(StreamInput in) throws IOException { + this.creatorType = in.readEnum(Creator.class); + this.creator = in.readString(); + } + + public String getCreator() { + return creator; + } + + public Creator getCreatorType() { + return creatorType; + } + + @Override + public String toString() { + return "CreatedBy {" + this.creatorType.getName() + "='" + this.creator + '\'' + '}'; + } + + @Override + public String getWriteableName() { + return "created_by"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(Creator.valueOf(creatorType.name())); + out.writeString(creator); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field(creatorType.getName(), creator).endObject(); + } + + public static CreatedBy fromXContent(XContentParser parser) throws IOException { + String creator = null; + Creator creatorType = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + creatorType = Creator.fromName(parser.currentName()); + } else if (token == XContentParser.Token.VALUE_STRING) { + creator = parser.text(); + } + } + + if (creator == null) { + throw new IllegalArgumentException(creatorType + " is required"); + } + + return new CreatedBy(creatorType, creator); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java new file mode 100644 index 0000000000..75e2415b93 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java @@ -0,0 +1,37 @@ +/* + * 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.spi.resources.sharing; + +/** + * This enum is used to store information about the creator of a resource. + * + * @opensearch.experimental + */ +public enum Creator { + USER("user"); + + private final String name; + + Creator(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public static Creator fromName(String name) { + for (Creator creator : values()) { + if (creator.name.equalsIgnoreCase(name)) { // Case-insensitive comparison + return creator; + } + } + throw new IllegalArgumentException("No enum constant for name: " + name); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java new file mode 100644 index 0000000000..77215071de --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java @@ -0,0 +1,31 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.spi.resources.sharing; + +/** + * Enum representing the recipients of a shared resource. + * It includes USERS, ROLES, and BACKEND_ROLES. + * + * @opensearch.experimental + */ +public enum Recipient { + USERS("users"), + ROLES("roles"), + BACKEND_ROLES("backend_roles"); + + private final String name; + + Recipient(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java new file mode 100644 index 0000000000..d3b916abc2 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java @@ -0,0 +1,24 @@ +/* + * 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.spi.resources.sharing; + +/** + * This class determines a type of recipient a resource can be shared with. + * An example type would be a user or a role. + * This class is used to determine the type of recipient a resource can be shared with. + * + * @opensearch.experimental + */ +public record RecipientType(String type) { + + @Override + public String toString() { + return type; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java new file mode 100644 index 0000000000..a1bdb89089 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java @@ -0,0 +1,39 @@ +/* + * 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.spi.resources.sharing; + +import java.util.HashMap; +import java.util.Map; + +/** + * This class determines a collection of recipient types a resource can be shared with. + * Allows addition of other recipient types in the future. + * + * @opensearch.experimental + */ +public final class RecipientTypeRegistry { + // TODO: Check what size should this be. A cap should be added to avoid infinite addition of objects + private static final Integer REGISTRY_MAX_SIZE = 20; + private static final Map REGISTRY = new HashMap<>(10); + + public static void registerRecipientType(String key, RecipientType recipientType) { + if (REGISTRY.size() == REGISTRY_MAX_SIZE) { + throw new IllegalArgumentException("RecipientTypeRegistry is full. Cannot register more recipient types."); + } + REGISTRY.put(key, recipientType); + } + + public static RecipientType fromValue(String value) { + RecipientType type = REGISTRY.get(value); + if (type == null) { + throw new IllegalArgumentException("Unknown RecipientType: " + value + ". Must be 1 of these: " + REGISTRY.values()); + } + return type; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java new file mode 100644 index 0000000000..3e00bc5d29 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java @@ -0,0 +1,202 @@ +/* + * 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.spi.resources.sharing; + +import java.io.IOException; +import java.util.Objects; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * Represents a resource sharing configuration that manages access control for OpenSearch resources. + * This class holds information about shared resources including their source, creator, and sharing permissions. + * The class maintains information about: + *
    + *
  • The source index where the resource is defined
  • + *
  • The unique identifier of the resource
  • + *
  • The creator's information
  • + *
  • The sharing permissions and recipients
  • + *
+ * + * @opensearch.experimental + * @see org.opensearch.security.spi.resources.sharing.CreatedBy + * @see org.opensearch.security.spi.resources.sharing.ShareWith + */ +public class ResourceSharing implements ToXContentFragment, NamedWriteable { + + /** + * The index where the resource is defined + */ + private String sourceIdx; + + /** + * The unique identifier of the resource + */ + private String resourceId; + + /** + * Information about who created the resource + */ + private CreatedBy createdBy; + + /** + * Information about with whom the resource is shared with + */ + private ShareWith shareWith; + + public ResourceSharing(String sourceIdx, String resourceId, CreatedBy createdBy, ShareWith shareWith) { + this.sourceIdx = sourceIdx; + this.resourceId = resourceId; + this.createdBy = createdBy; + this.shareWith = shareWith; + } + + public String getSourceIdx() { + return sourceIdx; + } + + public void setSourceIdx(String sourceIdx) { + this.sourceIdx = sourceIdx; + } + + public String getResourceId() { + return resourceId; + } + + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + + public CreatedBy getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(CreatedBy createdBy) { + this.createdBy = createdBy; + } + + public ShareWith getShareWith() { + return shareWith; + } + + public void setShareWith(ShareWith shareWith) { + this.shareWith = shareWith; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResourceSharing resourceSharing = (ResourceSharing) o; + return Objects.equals(getSourceIdx(), resourceSharing.getSourceIdx()) + && Objects.equals(getResourceId(), resourceSharing.getResourceId()) + && Objects.equals(getCreatedBy(), resourceSharing.getCreatedBy()) + && Objects.equals(getShareWith(), resourceSharing.getShareWith()); + } + + @Override + public int hashCode() { + return Objects.hash(getSourceIdx(), getResourceId(), getCreatedBy(), getShareWith()); + } + + @Override + public String toString() { + return "ShareableResource {" + + "sourceIdx='" + + sourceIdx + + '\'' + + ", resourceId='" + + resourceId + + '\'' + + ", createdBy=" + + createdBy + + ", sharedWith=" + + shareWith + + '}'; + } + + @Override + public String getWriteableName() { + return "resource_sharing"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(sourceIdx); + out.writeString(resourceId); + createdBy.writeTo(out); + if (shareWith != null) { + out.writeBoolean(true); + shareWith.writeTo(out); + } else { + out.writeBoolean(false); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject().field("source_idx", sourceIdx).field("resource_id", resourceId).field("created_by"); + createdBy.toXContent(builder, params); + if (shareWith != null && !shareWith.getSharedWithScopes().isEmpty()) { + builder.field("share_with"); + shareWith.toXContent(builder, params); + } + return builder.endObject(); + } + + public static ResourceSharing fromXContent(XContentParser parser) throws IOException { + String sourceIdx = null; + String resourceId = null; + CreatedBy createdBy = null; + ShareWith shareWith = null; + + String currentFieldName = null; + XContentParser.Token token; + + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + currentFieldName = parser.currentName(); + } else { + switch (Objects.requireNonNull(currentFieldName)) { + case "source_idx": + sourceIdx = parser.text(); + break; + case "resource_id": + resourceId = parser.text(); + break; + case "created_by": + createdBy = CreatedBy.fromXContent(parser); + break; + case "share_with": + shareWith = ShareWith.fromXContent(parser); + break; + default: + parser.skipChildren(); + break; + } + } + } + + validateRequiredField("source_idx", sourceIdx); + validateRequiredField("resource_id", resourceId); + validateRequiredField("created_by", createdBy); + + return new ResourceSharing(sourceIdx, resourceId, createdBy, shareWith); + } + + private static void validateRequiredField(String field, T value) { + if (value == null) { + throw new IllegalArgumentException(field + " is required"); + } + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java new file mode 100644 index 0000000000..267bb7ece0 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java @@ -0,0 +1,103 @@ +/* + * 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.spi.resources.sharing; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class contains information about whom a resource is shared with and at what scope. + * Example: + * "share_with": { + * "read_only": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * }, + * "read_write": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * } + * } + * + * @opensearch.experimental + */ +public class ShareWith implements ToXContentFragment, NamedWriteable { + + /** + * A set of objects representing the scopes and their associated users, roles, and backend roles. + */ + private final Set sharedWithScopes; + + public ShareWith(Set sharedWithScopes) { + this.sharedWithScopes = sharedWithScopes; + } + + public ShareWith(StreamInput in) throws IOException { + this.sharedWithScopes = in.readSet(SharedWithScope::new); + } + + public Set getSharedWithScopes() { + return sharedWithScopes; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + + for (SharedWithScope scope : sharedWithScopes) { + scope.toXContent(builder, params); + } + + return builder.endObject(); + } + + public static ShareWith fromXContent(XContentParser parser) throws IOException { + Set sharedWithScopes = new HashSet<>(); + + if (parser.currentToken() != XContentParser.Token.START_OBJECT) { + parser.nextToken(); + } + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + // Each field in the object represents a SharedWithScope + if (token == XContentParser.Token.FIELD_NAME) { + SharedWithScope scope = SharedWithScope.fromXContent(parser); + sharedWithScopes.add(scope); + } + } + + return new ShareWith(sharedWithScopes); + } + + @Override + public String getWriteableName() { + return "share_with"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeCollection(sharedWithScopes); + } + + @Override + public String toString() { + return "ShareWith " + sharedWithScopes; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java new file mode 100644 index 0000000000..1dfca103a3 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java @@ -0,0 +1,169 @@ +/* + * 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.spi.resources.sharing; + +import java.io.IOException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; + +/** + * This class represents the scope at which a resource is shared with for a particular scope. + * Example: + * "read_only": { + * "users": [], + * "roles": [], + * "backend_roles": [] + * } + * where "users", "roles" and "backend_roles" are the recipient entities + * + * @opensearch.experimental + */ +public class SharedWithScope implements ToXContentFragment, NamedWriteable { + + private final String scope; + + private final ScopeRecipients scopeRecipients; + + public SharedWithScope(String scope, ScopeRecipients scopeRecipients) { + this.scope = scope; + this.scopeRecipients = scopeRecipients; + } + + public SharedWithScope(StreamInput in) throws IOException { + this.scope = in.readString(); + this.scopeRecipients = new ScopeRecipients(in); + } + + public String getScope() { + return scope; + } + + public ScopeRecipients getSharedWithPerScope() { + return scopeRecipients; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.field(scope); + builder.startObject(); + + scopeRecipients.toXContent(builder, params); + + return builder.endObject(); + } + + public static SharedWithScope fromXContent(XContentParser parser) throws IOException { + String scope = parser.currentName(); + + parser.nextToken(); + + ScopeRecipients scopeRecipients = ScopeRecipients.fromXContent(parser); + + return new SharedWithScope(scope, scopeRecipients); + } + + @Override + public String toString() { + return "{" + scope + ": " + scopeRecipients + '}'; + } + + @Override + public String getWriteableName() { + return "shared_with_scope"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(scope); + out.writeNamedWriteable(scopeRecipients); + } + + /** + * This class represents the entities with whom a resource is shared with for a given scope. + * + * @opensearch.experimental + */ + public static class ScopeRecipients implements ToXContentFragment, NamedWriteable { + + private final Map> recipients; + + public ScopeRecipients(Map> recipients) { + if (recipients == null) { + throw new IllegalArgumentException("Recipients map cannot be null"); + } + this.recipients = recipients; + } + + public ScopeRecipients(StreamInput in) throws IOException { + this.recipients = in.readMap( + key -> RecipientTypeRegistry.fromValue(key.readString()), + input -> input.readSet(StreamInput::readString) + ); + } + + public Map> getRecipients() { + return recipients; + } + + @Override + public String getWriteableName() { + return "scope_recipients"; + } + + public static ScopeRecipients fromXContent(XContentParser parser) throws IOException { + Map> recipients = new HashMap<>(); + + XContentParser.Token token; + while ((token = parser.nextToken()) != XContentParser.Token.END_OBJECT) { + if (token == XContentParser.Token.FIELD_NAME) { + String fieldName = parser.currentName(); + RecipientType recipientType = RecipientTypeRegistry.fromValue(fieldName); + + parser.nextToken(); + Set values = new HashSet<>(); + while (parser.nextToken() != XContentParser.Token.END_ARRAY) { + values.add(parser.text()); + } + recipients.put(recipientType, values); + } + } + + return new ScopeRecipients(recipients); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeMap( + recipients, + (streamOutput, recipientType) -> streamOutput.writeString(recipientType.type()), + (streamOutput, strings) -> streamOutput.writeCollection(strings, StreamOutput::writeString) + ); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + if (recipients.isEmpty()) { + return builder; + } + for (Map.Entry> entry : recipients.entrySet()) { + builder.array(entry.getKey().type(), entry.getValue().toArray()); + } + return builder; + } + } +} diff --git a/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java new file mode 100644 index 0000000000..7d6eb5c61a --- /dev/null +++ b/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java @@ -0,0 +1,320 @@ +/* + * 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.spi.resources; + +import java.io.IOException; + +import org.hamcrest.MatcherAssert; +import org.junit.Test; + +import org.opensearch.common.io.stream.BytesStreamOutput; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.spi.resources.sharing.CreatedBy; +import org.opensearch.security.spi.resources.sharing.Creator; + +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.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Test class for CreatedBy class + * + * @opensearch.experimental + */ +public class CreatedByTests { + + private static final Creator CREATOR_TYPE = Creator.USER; + + @Test + public void testCreatedByConstructorWithValidUser() { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + + MatcherAssert.assertThat(expectedUser, is(equalTo(createdBy.getCreator()))); + } + + @Test + public void testCreatedByFromStreamInput() throws IOException { + String expectedUser = "testUser"; + + try (BytesStreamOutput out = new BytesStreamOutput()) { + out.writeEnum(Creator.valueOf(CREATOR_TYPE.name())); + out.writeString(expectedUser); + + StreamInput in = out.bytes().streamInput(); + + CreatedBy createdBy = new CreatedBy(in); + + MatcherAssert.assertThat(expectedUser, is(equalTo(createdBy.getCreator()))); + } + } + + @Test + public void testCreatedByWithEmptyStreamInput() throws IOException { + + try (StreamInput mockStreamInput = mock(StreamInput.class)) { + when(mockStreamInput.readString()).thenThrow(new IOException("EOF")); + + assertThrows(IOException.class, () -> new CreatedBy(mockStreamInput)); + } + } + + @Test + public void testCreatedByWithEmptyUser() { + + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); + MatcherAssert.assertThat("", equalTo(createdBy.getCreator())); + } + + @Test + public void testCreatedByWithIOException() throws IOException { + + try (StreamInput mockStreamInput = mock(StreamInput.class)) { + when(mockStreamInput.readString()).thenThrow(new IOException("Test IOException")); + + assertThrows(IOException.class, () -> new CreatedBy(mockStreamInput)); + } + } + + @Test + public void testCreatedByWithLongUsername() { + String longUsername = "a".repeat(10000); + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUsername); + MatcherAssert.assertThat(longUsername, equalTo(createdBy.getCreator())); + } + + @Test + public void testCreatedByWithUnicodeCharacters() { + String unicodeUsername = "用户こんにちは"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, unicodeUsername); + MatcherAssert.assertThat(unicodeUsername, equalTo(createdBy.getCreator())); + } + + @Test + public void testFromXContentThrowsExceptionWhenUserFieldIsMissing() throws IOException { + String emptyJson = "{}"; + IllegalArgumentException exception; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { + + exception = assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + + MatcherAssert.assertThat("null is required", equalTo(exception.getMessage())); + } + + @Test + public void testFromXContentWithEmptyInput() throws IOException { + String emptyJson = "{}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + @Test + public void testFromXContentWithExtraFields() throws IOException { + String jsonWithExtraFields = "{\"user\": \"testUser\", \"extraField\": \"value\"}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithExtraFields); + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + + @Test + public void testFromXContentWithIncorrectFieldType() throws IOException { + String jsonWithIncorrectType = "{\"user\": 12345}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithIncorrectType)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + @Test + public void testFromXContentWithEmptyUser() throws IOException { + String emptyJson = "{\"" + CREATOR_TYPE + "\": \"\" }"; + CreatedBy createdBy; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, emptyJson)) { + parser.nextToken(); + + createdBy = CreatedBy.fromXContent(parser); + } + + MatcherAssert.assertThat(CREATOR_TYPE, equalTo(createdBy.getCreatorType())); + MatcherAssert.assertThat("", equalTo(createdBy.getCreator())); + } + + @Test + public void testFromXContentWithNullUserValue() throws IOException { + String jsonWithNullUser = "{\"user\": null}"; + try (XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, jsonWithNullUser)) { + + assertThrows(IllegalArgumentException.class, () -> CreatedBy.fromXContent(parser)); + } + } + + @Test + public void testFromXContentWithValidUser() throws IOException { + String json = "{\"user\":\"testUser\"}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json); + + CreatedBy createdBy = CreatedBy.fromXContent(parser); + + MatcherAssert.assertThat(createdBy, notNullValue()); + MatcherAssert.assertThat("testUser", equalTo(createdBy.getCreator())); + } + + @Test + public void testGetCreatorReturnsCorrectValue() { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + + String actualUser = createdBy.getCreator(); + + MatcherAssert.assertThat(expectedUser, equalTo(actualUser)); + } + + @Test + public void testGetCreatorWithNullString() { + + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, null); + MatcherAssert.assertThat(createdBy.getCreator(), nullValue()); + } + + @Test + public void testGetWriteableNameReturnsCorrectString() { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, "testUser"); + MatcherAssert.assertThat("created_by", equalTo(createdBy.getWriteableName())); + } + + @Test + public void testToStringWithEmptyUser() { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); + String result = createdBy.toString(); + MatcherAssert.assertThat("CreatedBy {user=''}", equalTo(result)); + } + + @Test + public void testToStringWithNullUser() { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, (String) null); + String result = createdBy.toString(); + MatcherAssert.assertThat("CreatedBy {user='null'}", equalTo(result)); + } + + @Test + public void testToStringWithLongUserName() { + + String longUserName = "a".repeat(1000); + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUserName); + String result = createdBy.toString(); + MatcherAssert.assertThat(result.startsWith("CreatedBy {user='"), is(true)); + MatcherAssert.assertThat(result.endsWith("'}"), is(true)); + MatcherAssert.assertThat(1019, equalTo(result.length())); + } + + @Test + public void testToXContentWithEmptyUser() throws IOException { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, ""); + XContentBuilder builder = JsonXContent.contentBuilder(); + + createdBy.toXContent(builder, null); + String result = builder.toString(); + MatcherAssert.assertThat("{\"user\":\"\"}", equalTo(result)); + } + + @Test + public void testWriteToWithExceptionInStreamOutput() throws IOException { + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, "user1"); + try (StreamOutput failingOutput = new StreamOutput() { + @Override + public void writeByte(byte b) throws IOException { + throw new IOException("Simulated IO exception"); + } + + @Override + public void writeBytes(byte[] b, int offset, int length) throws IOException { + throw new IOException("Simulated IO exception"); + } + + @Override + public void flush() throws IOException { + + } + + @Override + public void close() throws IOException { + + } + + @Override + public void reset() throws IOException { + + } + }) { + + assertThrows(IOException.class, () -> createdBy.writeTo(failingOutput)); + } + } + + @Test + public void testWriteToWithLongUserName() throws IOException { + String longUserName = "a".repeat(65536); + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, longUserName); + BytesStreamOutput out = new BytesStreamOutput(); + createdBy.writeTo(out); + MatcherAssert.assertThat(out.size(), greaterThan(65536)); + } + + @Test + public void test_createdByToStringReturnsCorrectFormat() { + String testUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, testUser); + + String expected = "CreatedBy {user='" + testUser + "'}"; + String actual = createdBy.toString(); + + MatcherAssert.assertThat(expected, equalTo(actual)); + } + + @Test + public void test_toXContent_serializesCorrectly() throws IOException { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + XContentBuilder builder = XContentFactory.jsonBuilder(); + + createdBy.toXContent(builder, null); + + String expectedJson = "{\"user\":\"testUser\"}"; + MatcherAssert.assertThat(expectedJson, equalTo(builder.toString())); + } + + @Test + public void test_writeTo_writesUserCorrectly() throws IOException { + String expectedUser = "testUser"; + CreatedBy createdBy = new CreatedBy(CREATOR_TYPE, expectedUser); + + BytesStreamOutput out = new BytesStreamOutput(); + createdBy.writeTo(out); + + StreamInput in = out.bytes().streamInput(); + in.readString(); + String actualUser = in.readString(); + + MatcherAssert.assertThat(expectedUser, equalTo(actualUser)); + } + +} diff --git a/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java new file mode 100644 index 0000000000..8b0bfa3297 --- /dev/null +++ b/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java @@ -0,0 +1,43 @@ +/* + * 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.spi.resources; + +import org.hamcrest.MatcherAssert; +import org.junit.Test; + +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThrows; + +/** + * Tests for {@link RecipientTypeRegistry}. + * + * @opensearch.experimental + */ +public class RecipientTypeRegistryTests { + + @Test + public void testFromValue() { + RecipientTypeRegistry.registerRecipientType("ble1", new RecipientType("ble1")); + RecipientTypeRegistry.registerRecipientType("ble2", new RecipientType("ble2")); + + // Valid Value + RecipientType type = RecipientTypeRegistry.fromValue("ble1"); + MatcherAssert.assertThat(type, notNullValue()); + MatcherAssert.assertThat(type.type(), is(equalTo("ble1"))); + + // Invalid Value + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> RecipientTypeRegistry.fromValue("bleble")); + MatcherAssert.assertThat("Unknown RecipientType: bleble. Must be 1 of these: [ble1, ble2]", is(equalTo(exception.getMessage()))); + } +} diff --git a/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java b/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java new file mode 100644 index 0000000000..0e89e04f1c --- /dev/null +++ b/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java @@ -0,0 +1,284 @@ +/* + * 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.spi.resources; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.hamcrest.MatcherAssert; +import org.junit.Before; +import org.junit.Test; + +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; +import org.opensearch.security.spi.resources.sharing.ShareWith; +import org.opensearch.security.spi.resources.sharing.SharedWithScope; + +import org.mockito.Mockito; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.Assert.assertThrows; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test class for ShareWith class + * + * @opensearch.experimental + */ +public class ShareWithTests { + + @Before + public void setupResourceRecipientTypes() { + initializeRecipientTypes(); + } + + @Test + public void testFromXContentWhenCurrentTokenIsNotStartObject() throws IOException { + String json = "{\"read_only\": {\"users\": [\"user1\"], \"roles\": [], \"backend_roles\": []}}"; + XContentParser parser = JsonXContent.jsonXContent.createParser(null, null, json); + + parser.nextToken(); + + ShareWith shareWith = ShareWith.fromXContent(parser); + + MatcherAssert.assertThat(shareWith, notNullValue()); + Set sharedWithScopes = shareWith.getSharedWithScopes(); + MatcherAssert.assertThat(sharedWithScopes, notNullValue()); + MatcherAssert.assertThat(1, equalTo(sharedWithScopes.size())); + + SharedWithScope scope = sharedWithScopes.iterator().next(); + MatcherAssert.assertThat("read_only", equalTo(scope.getScope())); + + SharedWithScope.ScopeRecipients scopeRecipients = scope.getSharedWithPerScope(); + MatcherAssert.assertThat(scopeRecipients, notNullValue()); + Map> recipients = scopeRecipients.getRecipients(); + MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(), is(1)); + MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())), contains("user1")); + MatcherAssert.assertThat(recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.ROLES.getName())).size(), is(0)); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), + is(0) + ); + } + + @Test + public void testFromXContentWithEmptyInput() throws IOException { + String emptyJson = "{}"; + XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, null, emptyJson); + + ShareWith result = ShareWith.fromXContent(parser); + + MatcherAssert.assertThat(result, notNullValue()); + MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty())); + } + + @Test + public void testFromXContentWithStartObject() throws IOException { + XContentParser parser; + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.startObject() + .startObject(ResourceAccessScope.READ_ONLY) + .array("users", "user1", "user2") + .array("roles", "role1") + .array("backend_roles", "backend_role1") + .endObject() + .startObject(ResourceAccessScope.PUBLIC) + .array("users", "*") + .array("roles", "*") + .array("backend_roles", "*") + .endObject() + .endObject(); + + parser = JsonXContent.jsonXContent.createParser(null, null, builder.toString()); + } + + parser.nextToken(); + + ShareWith shareWith = ShareWith.fromXContent(parser); + + MatcherAssert.assertThat(shareWith, notNullValue()); + Set scopes = shareWith.getSharedWithScopes(); + MatcherAssert.assertThat(scopes.size(), equalTo(2)); + + for (SharedWithScope scope : scopes) { + SharedWithScope.ScopeRecipients perScope = scope.getSharedWithPerScope(); + Map> recipients = perScope.getRecipients(); + if (scope.getScope().equals(ResourceAccessScope.READ_ONLY)) { + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(), + is(2) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.ROLES.getName())).size(), + is(1) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), + is(1) + ); + } else if (scope.getScope().equals(ResourceAccessScope.PUBLIC)) { + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.USERS.getName())).size(), + is(1) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.ROLES.getName())).size(), + is(1) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), + is(1) + ); + } + } + } + + @Test + public void testFromXContentWithUnexpectedEndOfInput() throws IOException { + XContentParser mockParser = mock(XContentParser.class); + when(mockParser.currentToken()).thenReturn(XContentParser.Token.START_OBJECT); + when(mockParser.nextToken()).thenReturn(XContentParser.Token.END_OBJECT, (XContentParser.Token) null); + + ShareWith result = ShareWith.fromXContent(mockParser); + + MatcherAssert.assertThat(result, notNullValue()); + MatcherAssert.assertThat(result.getSharedWithScopes(), is(empty())); + } + + @Test + public void testToXContentBuildsCorrectly() throws IOException { + SharedWithScope scope = new SharedWithScope( + "scope1", + new SharedWithScope.ScopeRecipients(Map.of(new RecipientType("users"), Set.of("bleh"))) + ); + + Set scopes = new HashSet<>(); + scopes.add(scope); + + ShareWith shareWith = new ShareWith(scopes); + + XContentBuilder builder = JsonXContent.contentBuilder(); + + shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); + + String result = builder.toString(); + + String expected = "{\"scope1\":{\"users\":[\"bleh\"]}}"; + + MatcherAssert.assertThat(expected.length(), equalTo(result.length())); + MatcherAssert.assertThat(expected, equalTo(result)); + } + + @Test + public void testWriteToWithEmptySet() throws IOException { + Set emptySet = Collections.emptySet(); + ShareWith shareWith = new ShareWith(emptySet); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + shareWith.writeTo(mockOutput); + + verify(mockOutput).writeCollection(emptySet); + } + + @Test + public void testWriteToWithIOException() throws IOException { + Set set = new HashSet<>(); + set.add(new SharedWithScope("test", new SharedWithScope.ScopeRecipients(Map.of()))); + ShareWith shareWith = new ShareWith(set); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + doThrow(new IOException("Simulated IO exception")).when(mockOutput).writeCollection(set); + + assertThrows(IOException.class, () -> shareWith.writeTo(mockOutput)); + } + + @Test + public void testWriteToWithLargeSet() throws IOException { + Set largeSet = new HashSet<>(); + for (int i = 0; i < 10000; i++) { + largeSet.add(new SharedWithScope("scope" + i, new SharedWithScope.ScopeRecipients(Map.of()))); + } + ShareWith shareWith = new ShareWith(largeSet); + StreamOutput mockOutput = Mockito.mock(StreamOutput.class); + + shareWith.writeTo(mockOutput); + + verify(mockOutput).writeCollection(largeSet); + } + + @Test + public void test_fromXContent_emptyObject() throws IOException { + XContentParser parser; + try (XContentBuilder builder = XContentFactory.jsonBuilder()) { + builder.startObject().endObject(); + parser = XContentType.JSON.xContent().createParser(null, null, builder.toString()); + } + + ShareWith shareWith = ShareWith.fromXContent(parser); + + MatcherAssert.assertThat(shareWith.getSharedWithScopes(), is(empty())); + } + + @Test + public void test_writeSharedWithScopesToStream() throws IOException { + StreamOutput mockStreamOutput = Mockito.mock(StreamOutput.class); + + Set sharedWithScopes = new HashSet<>(); + sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.READ_ONLY, new SharedWithScope.ScopeRecipients(Map.of()))); + sharedWithScopes.add(new SharedWithScope(ResourceAccessScope.PUBLIC, new SharedWithScope.ScopeRecipients(Map.of()))); + + ShareWith shareWith = new ShareWith(sharedWithScopes); + + shareWith.writeTo(mockStreamOutput); + + verify(mockStreamOutput, times(1)).writeCollection(eq(sharedWithScopes)); + } + + private void initializeRecipientTypes() { + RecipientTypeRegistry.registerRecipientType("users", new RecipientType("users")); + RecipientTypeRegistry.registerRecipientType("roles", new RecipientType("roles")); + RecipientTypeRegistry.registerRecipientType("backend_roles", new RecipientType("backend_roles")); + } +} + +enum DefaultRecipientType { + USERS("users"), + ROLES("roles"), + BACKEND_ROLES("backend_roles"); + + private final String name; + + DefaultRecipientType(String name) { + this.name = name; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 0802cb856c..843553d971 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -116,6 +116,7 @@ import org.opensearch.indices.IndicesService; import org.opensearch.indices.SystemIndexDescriptor; import org.opensearch.plugins.ClusterPlugin; +import org.opensearch.plugins.ExtensiblePlugin; import org.opensearch.plugins.ExtensionAwarePlugin; import org.opensearch.plugins.IdentityPlugin; import org.opensearch.plugins.MapperPlugin; @@ -236,9 +237,10 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin implements ClusterPlugin, MapperPlugin, + IdentityPlugin, // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings ExtensionAwarePlugin, - IdentityPlugin + ExtensiblePlugin // CS-ENFORCE-SINGLE { @@ -1194,7 +1196,7 @@ public Collection createComponents( // NOTE: We need to create DefaultInterClusterRequestEvaluator before creating ConfigurationRepository since the latter requires // security index to be accessible which means - // communciation with other nodes is already up. However for the communication to be up, there needs to be trusted nodes_dn. Hence + // communication with other nodes is already up. However for the communication to be up, there needs to be trusted nodes_dn. Hence // the base values from opensearch.yml // is used to first establish trust between same cluster nodes and there after dynamic config is loaded if enabled. if (DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS.equals(className)) { @@ -2141,8 +2143,8 @@ public Collection getSystemIndexDescriptors(Settings sett ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); - final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); - return Collections.singletonList(systemIndexDescriptor); + final SystemIndexDescriptor securityIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); + return List.of(securityIndexDescriptor); } @Override @@ -2201,6 +2203,13 @@ private void tryAddSecurityProvider() { }); } + // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings + @Override + public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { + // Resource Sharing extensions will be loaded here + } + // CS-ENFORCE-SINGLE + public static class GuiceHolder implements LifecycleComponent { private static RepositoriesService repositoriesService; diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 307db9cbcd..633b85cff6 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -43,6 +43,7 @@ public class ConfigConstants { public static final String OPENDISTRO_SECURITY_CONFIG_PREFIX = "_opendistro_security_"; + public static final String SECURITY_SETTINGS_PREFIX = "plugins.security."; public static final String OPENDISTRO_SECURITY_CHANNEL_TYPE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "channel_type"; @@ -131,11 +132,11 @@ public class ConfigConstants { public static final String OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX = ".opendistro_security"; - public static final String SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = "plugins.security.enable_snapshot_restore_privilege"; + public static final String SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = SECURITY_SETTINGS_PREFIX + "enable_snapshot_restore_privilege"; public static final boolean SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE = true; - public static final String SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = - "plugins.security.check_snapshot_restore_write_privileges"; + public static final String SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = SECURITY_SETTINGS_PREFIX + + "check_snapshot_restore_write_privileges"; public static final boolean SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES = true; public static final Set SECURITY_SNAPSHOT_RESTORE_NEEDED_WRITE_PRIVILEGES = Collections.unmodifiableSet( new HashSet(Arrays.asList("indices:admin/create", "indices:data/write/index" @@ -143,37 +144,39 @@ public class ConfigConstants { )) ); - public static final String SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = "plugins.security.cert.intercluster_request_evaluator_class"; + public static final String SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = SECURITY_SETTINGS_PREFIX + + "cert.intercluster_request_evaluator_class"; public static final String OPENDISTRO_SECURITY_ACTION_NAME = OPENDISTRO_SECURITY_CONFIG_PREFIX + "action_name"; - public static final String SECURITY_AUTHCZ_ADMIN_DN = "plugins.security.authcz.admin_dn"; - public static final String SECURITY_CONFIG_INDEX_NAME = "plugins.security.config_index_name"; - public static final String SECURITY_AUTHCZ_IMPERSONATION_DN = "plugins.security.authcz.impersonation_dn"; - public static final String SECURITY_AUTHCZ_REST_IMPERSONATION_USERS = "plugins.security.authcz.rest_impersonation_user"; + public static final String SECURITY_AUTHCZ_ADMIN_DN = SECURITY_SETTINGS_PREFIX + "authcz.admin_dn"; + public static final String SECURITY_CONFIG_INDEX_NAME = SECURITY_SETTINGS_PREFIX + "config_index_name"; + public static final String SECURITY_AUTHCZ_IMPERSONATION_DN = SECURITY_SETTINGS_PREFIX + "authcz.impersonation_dn"; + public static final String SECURITY_AUTHCZ_REST_IMPERSONATION_USERS = SECURITY_SETTINGS_PREFIX + "authcz.rest_impersonation_user"; public static final String BCRYPT = "bcrypt"; public static final String PBKDF2 = "pbkdf2"; - public static final String SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS = "plugins.security.password.hashing.bcrypt.rounds"; + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS = SECURITY_SETTINGS_PREFIX + "password.hashing.bcrypt.rounds"; public static final int SECURITY_PASSWORD_HASHING_BCRYPT_ROUNDS_DEFAULT = 12; - public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR = "plugins.security.password.hashing.bcrypt.minor"; + public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR = SECURITY_SETTINGS_PREFIX + "password.hashing.bcrypt.minor"; public static final String SECURITY_PASSWORD_HASHING_BCRYPT_MINOR_DEFAULT = "Y"; - public static final String SECURITY_PASSWORD_HASHING_ALGORITHM = "plugins.security.password.hashing.algorithm"; + public static final String SECURITY_PASSWORD_HASHING_ALGORITHM = SECURITY_SETTINGS_PREFIX + "password.hashing.algorithm"; public static final String SECURITY_PASSWORD_HASHING_ALGORITHM_DEFAULT = BCRYPT; - public static final String SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS = "plugins.security.password.hashing.pbkdf2.iterations"; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS = SECURITY_SETTINGS_PREFIX + + "password.hashing.pbkdf2.iterations"; public static final int SECURITY_PASSWORD_HASHING_PBKDF2_ITERATIONS_DEFAULT = 600_000; - public static final String SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH = "plugins.security.password.hashing.pbkdf2.length"; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH = SECURITY_SETTINGS_PREFIX + "password.hashing.pbkdf2.length"; public static final int SECURITY_PASSWORD_HASHING_PBKDF2_LENGTH_DEFAULT = 256; - public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION = "plugins.security.password.hashing.pbkdf2.function"; + public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION = SECURITY_SETTINGS_PREFIX + "password.hashing.pbkdf2.function"; public static final String SECURITY_PASSWORD_HASHING_PBKDF2_FUNCTION_DEFAULT = Hmac.SHA256.name(); - public static final String SECURITY_AUDIT_TYPE_DEFAULT = "plugins.security.audit.type"; - public static final String SECURITY_AUDIT_CONFIG_DEFAULT = "plugins.security.audit.config"; - public static final String SECURITY_AUDIT_CONFIG_ROUTES = "plugins.security.audit.routes"; - public static final String SECURITY_AUDIT_CONFIG_ENDPOINTS = "plugins.security.audit.endpoints"; - public static final String SECURITY_AUDIT_THREADPOOL_SIZE = "plugins.security.audit.threadpool.size"; - public static final String SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN = "plugins.security.audit.threadpool.max_queue_len"; + public static final String SECURITY_AUDIT_TYPE_DEFAULT = SECURITY_SETTINGS_PREFIX + "audit.type"; + public static final String SECURITY_AUDIT_CONFIG_DEFAULT = SECURITY_SETTINGS_PREFIX + "audit.config"; + public static final String SECURITY_AUDIT_CONFIG_ROUTES = SECURITY_SETTINGS_PREFIX + "audit.routes"; + public static final String SECURITY_AUDIT_CONFIG_ENDPOINTS = SECURITY_SETTINGS_PREFIX + "audit.endpoints"; + public static final String SECURITY_AUDIT_THREADPOOL_SIZE = SECURITY_SETTINGS_PREFIX + "audit.threadpool.size"; + public static final String SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN = SECURITY_SETTINGS_PREFIX + "audit.threadpool.max_queue_len"; public static final String OPENDISTRO_SECURITY_AUDIT_LOG_REQUEST_BODY = "opendistro_security.audit.log_request_body"; public static final String OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES = "opendistro_security.audit.resolve_indices"; public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_REST = "opendistro_security.audit.enable_rest"; @@ -188,13 +191,13 @@ 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 SECURITY_AUDIT_IGNORE_HEADERS = SECURITY_SETTINGS_PREFIX + "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; public static final String OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS = "opendistro_security.audit.exclude_sensitive_headers"; - public static final String SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX = "plugins.security.audit.config."; + public static final String SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX = SECURITY_SETTINGS_PREFIX + "audit.config."; // Internal Opensearch data_stream public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_NAME = "data_stream.name"; @@ -237,31 +240,31 @@ public class ConfigConstants { public static final String SECURITY_AUDIT_LOG4J_LEVEL = "log4j.level"; // retry - public static final String SECURITY_AUDIT_RETRY_COUNT = "plugins.security.audit.config.retry_count"; - public static final String SECURITY_AUDIT_RETRY_DELAY_MS = "plugins.security.audit.config.retry_delay_ms"; + public static final String SECURITY_AUDIT_RETRY_COUNT = SECURITY_SETTINGS_PREFIX + "audit.config.retry_count"; + public static final String SECURITY_AUDIT_RETRY_DELAY_MS = SECURITY_SETTINGS_PREFIX + "audit.config.retry_delay_ms"; - public static final String SECURITY_KERBEROS_KRB5_FILEPATH = "plugins.security.kerberos.krb5_filepath"; - public static final String SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH = "plugins.security.kerberos.acceptor_keytab_filepath"; - public static final String SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL = "plugins.security.kerberos.acceptor_principal"; - public static final String SECURITY_CERT_OID = "plugins.security.cert.oid"; - public static final String SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = - "plugins.security.cert.intercluster_request_evaluator_class"; - public static final String SECURITY_ADVANCED_MODULES_ENABLED = "plugins.security.advanced_modules_enabled"; - 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_KERBEROS_KRB5_FILEPATH = SECURITY_SETTINGS_PREFIX + "kerberos.krb5_filepath"; + public static final String SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH = SECURITY_SETTINGS_PREFIX + "kerberos.acceptor_keytab_filepath"; + public static final String SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL = SECURITY_SETTINGS_PREFIX + "kerberos.acceptor_principal"; + public static final String SECURITY_CERT_OID = SECURITY_SETTINGS_PREFIX + "cert.oid"; + public static final String SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = SECURITY_SETTINGS_PREFIX + + "cert.intercluster_request_evaluator_class"; + public static final String SECURITY_ADVANCED_MODULES_ENABLED = SECURITY_SETTINGS_PREFIX + "advanced_modules_enabled"; + public static final String SECURITY_NODES_DN = SECURITY_SETTINGS_PREFIX + "nodes_dn"; + public static final String SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED = SECURITY_SETTINGS_PREFIX + "nodes_dn_dynamic_config_enabled"; + public static final String SECURITY_DISABLED = SECURITY_SETTINGS_PREFIX + "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_CACHE_TTL_MINUTES = SECURITY_SETTINGS_PREFIX + "cache.ttl_minutes"; + public static final String SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES = SECURITY_SETTINGS_PREFIX + "allow_unsafe_democertificates"; + public static final String SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX = SECURITY_SETTINGS_PREFIX + "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_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE = SECURITY_SETTINGS_PREFIX + + "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"; + public static final String SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST = SECURITY_SETTINGS_PREFIX + + "background_init_if_securityindex_not_exist"; - public static final String SECURITY_ROLES_MAPPING_RESOLUTION = "plugins.security.roles_mapping_resolution"; + public static final String SECURITY_ROLES_MAPPING_RESOLUTION = SECURITY_SETTINGS_PREFIX + "roles_mapping_resolution"; public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY = "opendistro_security.compliance.history.write.metadata_only"; @@ -280,21 +283,22 @@ public class ConfigConstants { public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED = "opendistro_security.compliance.history.external_config_enabled"; public static final String OPENDISTRO_SECURITY_SOURCE_FIELD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "source_field_context"; - public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = - "plugins.security.compliance.disable_anonymous_authentication"; - public static final String SECURITY_COMPLIANCE_IMMUTABLE_INDICES = "plugins.security.compliance.immutable_indices"; - public static final String SECURITY_COMPLIANCE_SALT = "plugins.security.compliance.salt"; + public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = SECURITY_SETTINGS_PREFIX + + "compliance.disable_anonymous_authentication"; + public static final String SECURITY_COMPLIANCE_IMMUTABLE_INDICES = SECURITY_SETTINGS_PREFIX + "compliance.immutable_indices"; + public static final String SECURITY_COMPLIANCE_SALT = SECURITY_SETTINGS_PREFIX + "compliance.salt"; public static final String SECURITY_COMPLIANCE_SALT_DEFAULT = "e1ukloTsQlOgPquJ";// 16 chars public static final String SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED = "opendistro_security.compliance.history.internal_config_enabled"; - public static final String SECURITY_SSL_ONLY = "plugins.security.ssl_only"; + public static final String SECURITY_SSL_ONLY = SECURITY_SETTINGS_PREFIX + "ssl_only"; public static final String SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED = "plugins.security_config.ssl_dual_mode_enabled"; public static final String SECURITY_SSL_DUAL_MODE_SKIP_SECURITY = OPENDISTRO_SECURITY_CONFIG_PREFIX + "passive_security"; public static final String LEGACY_OPENDISTRO_SECURITY_CONFIG_SSL_DUAL_MODE_ENABLED = "opendistro_security_config.ssl_dual_mode_enabled"; - public static final String SECURITY_SSL_CERT_RELOAD_ENABLED = "plugins.security.ssl_cert_reload_enabled"; - public static final String SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED = "plugins.security.ssl.certificates_hot_reload.enabled"; - public static final String SECURITY_DISABLE_ENVVAR_REPLACEMENT = "plugins.security.disable_envvar_replacement"; - public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = "plugins.security.dfm_empty_overrides_all"; + public static final String SECURITY_SSL_CERT_RELOAD_ENABLED = SECURITY_SETTINGS_PREFIX + "ssl_cert_reload_enabled"; + public static final String SECURITY_SSL_CERTIFICATES_HOT_RELOAD_ENABLED = SECURITY_SETTINGS_PREFIX + + "ssl.certificates_hot_reload.enabled"; + public static final String SECURITY_DISABLE_ENVVAR_REPLACEMENT = SECURITY_SETTINGS_PREFIX + "disable_envvar_replacement"; + public static final String SECURITY_DFM_EMPTY_OVERRIDES_ALL = SECURITY_SETTINGS_PREFIX + "dfm_empty_overrides_all"; public enum RolesMappingResolution { MAPPING_ONLY, @@ -302,43 +306,45 @@ public enum RolesMappingResolution { BOTH } - public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = "plugins.security.filter_securityindex_from_all_requests"; - public static final String SECURITY_DLS_MODE = "plugins.security.dls.mode"; + public static final String SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS = SECURITY_SETTINGS_PREFIX + + "filter_securityindex_from_all_requests"; + public static final String SECURITY_DLS_MODE = SECURITY_SETTINGS_PREFIX + "dls.mode"; // REST API - public static final String SECURITY_RESTAPI_ROLES_ENABLED = "plugins.security.restapi.roles_enabled"; - public static final String SECURITY_RESTAPI_ADMIN_ENABLED = "plugins.security.restapi.admin.enabled"; - public static final String SECURITY_RESTAPI_ENDPOINTS_DISABLED = "plugins.security.restapi.endpoints_disabled"; - public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX = "plugins.security.restapi.password_validation_regex"; - public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE = - "plugins.security.restapi.password_validation_error_message"; - public static final String SECURITY_RESTAPI_PASSWORD_MIN_LENGTH = "plugins.security.restapi.password_min_length"; - public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH = - "plugins.security.restapi.password_score_based_validation_strength"; + public static final String SECURITY_RESTAPI_ROLES_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.roles_enabled"; + public static final String SECURITY_RESTAPI_ADMIN_ENABLED = SECURITY_SETTINGS_PREFIX + "restapi.admin.enabled"; + public static final String SECURITY_RESTAPI_ENDPOINTS_DISABLED = SECURITY_SETTINGS_PREFIX + "restapi.endpoints_disabled"; + public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX = SECURITY_SETTINGS_PREFIX + "restapi.password_validation_regex"; + public static final String SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE = SECURITY_SETTINGS_PREFIX + + "restapi.password_validation_error_message"; + public static final String SECURITY_RESTAPI_PASSWORD_MIN_LENGTH = SECURITY_SETTINGS_PREFIX + "restapi.password_min_length"; + public static final String SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH = SECURITY_SETTINGS_PREFIX + + "restapi.password_score_based_validation_strength"; // 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 = - "plugins.security.unsupported.passive_intertransport_auth_initially"; - public static final String SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED = - "plugins.security.unsupported.restore.securityindex.enabled"; - public static final String SECURITY_UNSUPPORTED_INJECT_USER_ENABLED = "plugins.security.unsupported.inject_user.enabled"; - public static final String SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED = "plugins.security.unsupported.inject_user.admin.enabled"; - public static final String SECURITY_UNSUPPORTED_ALLOW_NOW_IN_DLS = "plugins.security.unsupported.allow_now_in_dls"; - - public static final String SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION = - "plugins.security.unsupported.restapi.allow_securityconfig_modification"; - public static final String SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES = "plugins.security.unsupported.load_static_resources"; - public static final String SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG = "plugins.security.unsupported.accept_invalid_config"; - - public static final String SECURITY_PROTECTED_INDICES_ENABLED_KEY = "plugins.security.protected_indices.enabled"; + public static final String SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX + + "unsupported.disable_rest_auth_initially"; + public static final String SECURITY_UNSUPPORTED_DELAY_INITIALIZATION_SECONDS = SECURITY_SETTINGS_PREFIX + + "unsupported.delay_initialization_seconds"; + public static final String SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX + + "unsupported.disable_intertransport_auth_initially"; + public static final String SECURITY_UNSUPPORTED_PASSIVE_INTERTRANSPORT_AUTH_INITIALLY = SECURITY_SETTINGS_PREFIX + + "unsupported.passive_intertransport_auth_initially"; + public static final String SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED = SECURITY_SETTINGS_PREFIX + + "unsupported.restore.securityindex.enabled"; + public static final String SECURITY_UNSUPPORTED_INJECT_USER_ENABLED = SECURITY_SETTINGS_PREFIX + "unsupported.inject_user.enabled"; + public static final String SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED = SECURITY_SETTINGS_PREFIX + + "unsupported.inject_user.admin.enabled"; + public static final String SECURITY_UNSUPPORTED_ALLOW_NOW_IN_DLS = SECURITY_SETTINGS_PREFIX + "unsupported.allow_now_in_dls"; + + public static final String SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION = SECURITY_SETTINGS_PREFIX + + "unsupported.restapi.allow_securityconfig_modification"; + public static final String SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES = SECURITY_SETTINGS_PREFIX + "unsupported.load_static_resources"; + public static final String SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG = SECURITY_SETTINGS_PREFIX + "unsupported.accept_invalid_config"; + + public static final String SECURITY_PROTECTED_INDICES_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.enabled"; public static final Boolean SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT = false; - public static final String SECURITY_PROTECTED_INDICES_KEY = "plugins.security.protected_indices.indices"; + public static final String SECURITY_PROTECTED_INDICES_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.indices"; public static final List SECURITY_PROTECTED_INDICES_DEFAULT = Collections.emptyList(); - public static final String SECURITY_PROTECTED_INDICES_ROLES_KEY = "plugins.security.protected_indices.roles"; + public static final String SECURITY_PROTECTED_INDICES_ROLES_KEY = SECURITY_SETTINGS_PREFIX + "protected_indices.roles"; public static final List SECURITY_PROTECTED_INDICES_ROLES_DEFAULT = Collections.emptyList(); // Roles injection for plugins @@ -352,19 +358,20 @@ public enum RolesMappingResolution { // System indices settings public static final String SYSTEM_INDEX_PERMISSION = "system:admin/system_index"; - public static final String SECURITY_SYSTEM_INDICES_ENABLED_KEY = "plugins.security.system_indices.enabled"; + public static final String SECURITY_SYSTEM_INDICES_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + "system_indices.enabled"; public static final Boolean SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT = false; - public static final String SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY = "plugins.security.system_indices.permission.enabled"; + public static final String SECURITY_SYSTEM_INDICES_PERMISSIONS_ENABLED_KEY = SECURITY_SETTINGS_PREFIX + + "system_indices.permission.enabled"; public static final Boolean SECURITY_SYSTEM_INDICES_PERMISSIONS_DEFAULT = false; - public static final String SECURITY_SYSTEM_INDICES_KEY = "plugins.security.system_indices.indices"; + public static final String SECURITY_SYSTEM_INDICES_KEY = SECURITY_SETTINGS_PREFIX + "system_indices.indices"; public static final List SECURITY_SYSTEM_INDICES_DEFAULT = Collections.emptyList(); - public static final String SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT = "plugins.security.masked_fields.algorithm.default"; + public static final String SECURITY_MASKED_FIELDS_ALGORITHM_DEFAULT = SECURITY_SETTINGS_PREFIX + "masked_fields.algorithm.default"; public static final String TENANCY_PRIVATE_TENANT_NAME = "private"; public static final String TENANCY_GLOBAL_TENANT_NAME = "global"; public static final String TENANCY_GLOBAL_TENANT_DEFAULT_NAME = ""; - public static final String USE_JDK_SERIALIZATION = "plugins.security.use_jdk_serialization"; + public static final String USE_JDK_SERIALIZATION = SECURITY_SETTINGS_PREFIX + "use_jdk_serialization"; // On-behalf-of endpoints settings // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings