From ccc5022d8f1b2d0c9fff22477430602e7b1898bc Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 17 Mar 2025 17:21:14 -0400 Subject: [PATCH 1/7] Introduces resource sharing and access control SPI Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 191 ++++++++--- .github/workflows/maven-publish.yml | 2 +- .gitignore | 4 - build.gradle | 92 +++-- scripts/build.sh | 4 + settings.gradle | 3 + spi/README.md | 167 +++++++++ spi/build.gradle | 86 +++++ .../security/spi/resources/Resource.java | 27 ++ .../spi/resources/ResourceAccessScope.java | 38 +++ .../spi/resources/ResourceParser.java | 29 ++ .../resources/ResourceSharingExtension.java | 35 ++ .../exceptions/ResourceSharingException.java | 60 ++++ .../security/spi/resources/package-info.java | 15 + .../spi/resources/sharing/CreatedBy.java | 88 +++++ .../spi/resources/sharing/Creator.java | 37 ++ .../spi/resources/sharing/Recipient.java | 31 ++ .../spi/resources/sharing/RecipientType.java | 24 ++ .../sharing/RecipientTypeRegistry.java | 39 +++ .../resources/sharing/ResourceSharing.java | 202 +++++++++++ .../spi/resources/sharing/ShareWith.java | 103 ++++++ .../resources/sharing/SharedWithScope.java | 169 +++++++++ .../spi/resources/CreatedByTests.java | 320 ++++++++++++++++++ .../resources/RecipientTypeRegistryTests.java | 43 +++ .../spi/resources/ShareWithTests.java | 284 ++++++++++++++++ .../security/OpenSearchSecurityPlugin.java | 17 +- .../security/support/ConfigConstants.java | 185 +++++----- 27 files changed, 2133 insertions(+), 162 deletions(-) create mode 100644 spi/README.md create mode 100644 spi/build.gradle create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/Resource.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/package-info.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java create mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java create mode 100644 spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java create mode 100644 spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java create mode 100644 spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5a41062883..7b1b82e300 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,9 +32,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: name: test - needs: generate-test-list + needs: [generate-test-list, publish-components-to-maven-local] strategy: fail-fast: false matrix: @@ -53,6 +79,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: @@ -68,7 +102,7 @@ jobs: ./build/reports/ report-coverage: - needs: ["test", "integration-tests"] + needs: ["test", "integration-tests", "spi-tests"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -91,7 +125,6 @@ jobs: fail_ci_if_error: true verbose: true - integration-tests: name: integration-tests strategy: @@ -111,12 +144,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() @@ -125,10 +166,52 @@ jobs: path: | ./build/reports/ + spi-tests: + name: spi-tests + needs: publish-components-to-maven-local + strategy: + fail-fast: false + matrix: + jdk: [21] + platform: [ubuntu-latest, 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: @@ -146,12 +229,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 @@ -214,40 +305,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 c7ba88a881..9d803c03f2 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..2d4d13f989 --- /dev/null +++ b/spi/README.md @@ -0,0 +1,167 @@ +# **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.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/Resource.java b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java new file mode 100644 index 0000000000..72e0b7b5d1 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.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 resources + * + * @opensearch.experimental + */ +public interface Resource extends NamedWriteable, ToXContentFragment { + /** + * Abstract method to get the resource name. + * Must be implemented by plugins defining resources. + * + * @return resource name + */ + String getResourceName(); +} 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..c3b54a8c23 --- /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 RESTRICTED = "restricted"; + String PUBLIC = "public"; + + 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/ResourceParser.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java new file mode 100644 index 0000000000..b02269322e --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.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 resources from XContentParser + * @param the type of resource to be parsed + * + * @opensearch.experimental + */ +public interface ResourceParser { + /** + * Parse source bytes supplied by the parser to a desired Resource type + * @param parser to parser bytes-ref json input + * @return the parsed object of Resource 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/ResourceSharingExtension.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java new file mode 100644 index 0000000000..bbfc802d82 --- /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 + */ + ResourceParser getResourceParser(); +} 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..560669112b --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.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 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() { + 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/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..1690213872 --- /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 "Resource {" + + "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..d7ffa0ce5e --- /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.RESTRICTED) + .array("users", "user1", "user2") + .array("roles", "role1") + .array("backend_roles", "backend_role1") + .endObject() + .startObject(ResourceAccessScope.PUBLIC) + .array("users", "user3") + .array("roles", "role2", "role3") + .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.RESTRICTED)) { + 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(2) + ); + MatcherAssert.assertThat( + recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), + is(0) + ); + } + } + } + + @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.RESTRICTED, 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 From 6b83ebf9dceb30997ea893530aee1161f7823254 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Mon, 17 Mar 2025 17:37:28 -0400 Subject: [PATCH 2/7] Introduces a resource sharing client and completes resource access control implementation in common package Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 37 +- RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md | 464 ++++++ build.gradle | 5 +- client/README.md | 233 +++ client/build.gradle | 101 ++ .../resources/ResourceSharingClient.java | 65 + .../resources/ResourceSharingNodeClient.java | 186 +++ .../client/resources/package-info.java | 14 + common/build.gradle | 91 ++ .../security/common/DefaultObjectMapper.java | 298 ++++ .../common/auditlog/impl/AuditCategory.java | 40 + .../common}/auth/UserSubjectImpl.java | 12 +- .../common/configuration/AdminDNs.java | 162 ++ .../common/dlic/rest/api/Responses.java | 106 ++ .../security/common/package-info.java | 15 + .../resources/ResourceAccessHandler.java | 549 +++++++ .../resources/ResourceIndexListener.java | 123 ++ .../common/resources/ResourcePluginInfo.java | 58 + .../common/resources/ResourceProvider.java | 21 + .../resources/ResourceSharingConstants.java | 21 + .../ResourceSharingIndexHandler.java | 1402 +++++++++++++++++ ...ourceSharingIndexManagementRepository.java | 59 + .../resources/rest/ResourceAccessAction.java | 28 + .../resources/rest/ResourceAccessRequest.java | 236 +++ .../rest/ResourceAccessRequestParams.java | 32 + .../rest/ResourceAccessResponse.java | 98 ++ .../rest/ResourceAccessRestAction.java | 151 ++ .../rest/ResourceAccessTransportAction.java | 117 ++ .../common/support/ConfigConstants.java | 404 +++++ .../security/common/support/Utils.java | 285 ++++ .../common/support/WildcardMatcher.java | 556 +++++++ .../security/common/user/AuthCredentials.java | 254 +++ .../common/user/CustomAttributesAware.java | 34 + .../opensearch/security/common/user/User.java | 312 ++++ scripts/build.sh | 2 + settings.gradle | 6 + .../security/OpenSearchSecurityPlugin.java | 105 +- .../security/auth/BackendRegistry.java | 25 +- .../security/dlic/rest/support/Utils.java | 2 + .../security/support/ConfigConstants.java | 4 + .../security/IndexIntegrationTests.java | 11 +- .../security/SlowIntegrationTests.java | 1 + .../security/auth/UserSubjectImplTests.java | 3 +- 43 files changed, 6702 insertions(+), 26 deletions(-) create mode 100644 RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md create mode 100644 client/README.md create mode 100644 client/build.gradle create mode 100644 client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java create mode 100644 client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java create mode 100644 client/src/main/java/org/opensearch/security/client/resources/package-info.java create mode 100644 common/build.gradle create mode 100644 common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java create mode 100644 common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java rename {src/main/java/org/opensearch/security => common/src/main/java/org/opensearch/security/common}/auth/UserSubjectImpl.java (83%) create mode 100644 common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java create mode 100644 common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java create mode 100644 common/src/main/java/org/opensearch/security/common/package-info.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java create mode 100644 common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java create mode 100644 common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java create mode 100644 common/src/main/java/org/opensearch/security/common/support/Utils.java create mode 100644 common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java create mode 100644 common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java create mode 100644 common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java create mode 100644 common/src/main/java/org/opensearch/security/common/user/User.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b1b82e300..3ed86681fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,8 @@ jobs: run: | ./gradlew clean \ :opensearch-resource-sharing-spi:publishToMavenLocal \ + :opensearch-security-common:publishToMavenLocal \ + :opensearch-security-client:publishToMavenLocal \ -Dbuild.snapshot=false - name: Cache artifacts for dependent jobs @@ -335,31 +337,54 @@ jobs: ./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 + # Publish Common + ./gradlew clean :opensearch-security-common:publishToMavenLocal && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar + ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot-all.jar + ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-all.jar + ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT-all.jar + + # Publish Client + ./gradlew clean :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version-all.jar + ./gradlew clean :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot-all.jar + ./gradlew clean :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_only_number-$test_qualifier-all.jar + ./gradlew clean :opensearch-security-client:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./client/build/libs/opensearch-security-client-$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 + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version.jar && \ + test -s ./client/build/libs/opensearch-security-client-$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 + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_no_snapshot.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot.jar && \ + test -s ./client/build/libs/opensearch-security-client-$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 + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier.jar && \ + test -s ./client/build/libs/opensearch-security-client-$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 + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar && \ + test -s ./client/build/libs/opensearch-security-client-$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 + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar ./gradlew clean publishShadowPublicationToMavenLocal && \ - test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar + test -s ./spi/build/libs/opensearch-resource-sharing-spi-$security_plugin_version-all.jar && \ + test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar && \ + test -s ./client/build/libs/opensearch-security-client-$security_plugin_version-all.jar - name: List files in build directory on failure if: failure() diff --git a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md new file mode 100644 index 0000000000..42c0c61731 --- /dev/null +++ b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md @@ -0,0 +1,464 @@ +# **Resource Sharing and Access Control in OpenSearch** + +This guide provides an **in-depth overview** for **plugin developers**, covering the **features, setup, and utilization** of the **Resource Sharing and Access Control** functionality in OpenSearch. + +## **1. What is the Feature?** +The **Resource Sharing and Access Control** feature in OpenSearch Security Plugin enables fine-grained access management for resources declared by plugins. It allows: +- Users to **share and revoke access** to their own resources. +- **Super admins** to access all resources. +- Plugins to **define and manage resource access** via a standardized interface. + +This feature ensures **secure** and **controlled** access to resources while leveraging existing **index-level authorization** in OpenSearch. + +--- + +## **2. What are the Components?** +This feature introduces **two primary components** for plugin developers: + +### **1. `opensearch-security-client`** +- Provides a client with methods for **resource access control**. +- Plugins must declare a **dependency** on this client to integrate with security features. + +### **2. `opensearch-resource-sharing-spi`** +- A **Service Provider Interface (SPI)** that plugins must implement to declare themselves as **Resource Plugins**. +- The security plugin keeps track of these plugins (similar to how JobScheduler tracks `JobSchedulerExtension`). + +### **Plugin Implementation Requirements:** + +- This feature is marked as **`@opensearch.experimental`** and can be toggled using the feature flag: **`plugins.security.resource_sharing.enabled`**, which is **enabled by default**. +- **Resource indices must be system indices**, and **system index protection must be enabled** (`plugins.security.system_indices.enabled: true`) to prevent unauthorized direct access. +- Plugins must declare dependencies on **`opensearch-security-client`** and **`opensearch-resource-sharing-spi`** in their `build.gradle`. + +### **Plugin Implementation Requirements** +Each plugin must: +- **Declare a dependency** on `opensearch-security-client` package: +```build.gradle +implementation group: 'org.opensearch', name:'opensearch-security-client', version: "${opensearch_build}" +``` +- **Extend** `opensearch-security` plugin with optional flag: +```build.gradle +opensearchplugin { + name '' + description '' + classname '' + extendedPlugins = ['opensearch-security;optional=true', ] +} +``` +- **Implement** the `ResourceSharingExtension` class. +- **Ensure** that its declared resources implement the `Resource` interface. +- **Provide a resource parser**, which the security plugin uses to extract resource details from the resource index. +- **Register itself** in `META-INF/services` by creating the following file: + ``` + src/main/resources/META-INF/services/org.opensearch.security.spi.ResourceSharingExtension + ``` + - This file must contain a **single line** specifying the **fully qualified class name** of the plugin’s `ResourceSharingExtension` implementation, e.g.: + ``` + org.opensearch.sample.SampleResourcePlugin + ``` +--- + +## **3. Feature Flag** +This feature is controlled by the following flag: + +- **Feature flag:** `plugins.security.resource_sharing.enabled` +- **Default value:** `true` +- **How to disable?** Set the flag to `false` in the opensearch configuration: + ```yaml + plugins.security.resource_sharing.enabled: false + ``` + +--- + +## **4. Declaring a Resource Plugin and Using the Client for Access Control** +### **Declaring a Plugin as a Resource Plugin** +To integrate with the security plugin, your plugin must: +1. Extend `ResourceSharingExtension` and implement required methods. +2. Implement the `Resource` interface for resource declaration. +3. Implement a resource parser to extract resource details. + +[`opensearch-resource-sharing-spi` README.md](./spi/README.md) is a great resource to learn more about the components of SPI and how to set up. + +Tip: Refer to the `org.opensearch.sample.SampleResourcePlugin` class to understand the setup in further detail. + +Example usage: +```java + +public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, ResourceSharingExtension { + + // override any 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(); + } +} +``` + + +### **Calling Access Control Methods from the ResourceSharingClient Client** +Plugins must **declare a dependency** on `opensearch-security-client` and use it to call access control methods. +The client provides **four access control methods** for plugins. For detailed usage and implementation, refer to the [`opensearch-security-client` README.md](./client/README.md) + + +Tip: Refer to the `org.opensearch.sample.resource.client.ResourceSharingClientAccessor` class to understand the client setup in further detail. + +Example usage: +```java + @Override +void doExecute(Task task, ShareResourceRequest request, ActionListener listener) { + if (request.getResourceId() == null || request.getResourceId().isEmpty()) { + listener.onFailure(new IllegalArgumentException("Resource ID cannot be null or empty")); + return; + } + + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); + resourceSharingClient.shareResource( + request.getResourceId(), + RESOURCE_INDEX_NAME, + request.getShareWith(), + ActionListener.wrap(sharing -> { + ShareResourceResponse response = new ShareResourceResponse(sharing.getShareWith()); + listener.onResponse(response); + }, listener::onFailure) + ); +} +``` + + +--- + +## **5. What are Scopes?** + +This feature introduces a new **sharing mechanism** called **scopes**. Scopes define **the level of access** granted to users for a resource. They are **defined and maintained by plugins**, and the security plugin does **not** interpret or enforce their specific meanings. This approach gives plugins the **flexibility** to define scope names and behaviors based on their use case. + +Each plugin must **document its scope definitions** so that users understand the **sharing semantics** and how different scopes affect access control. + +Scopes enable **granular access control**, allowing resources to be shared with **customized permission levels**, making the system more flexible and adaptable to different use cases. + +### **Common Scopes for Plugins to declare** +| Scope | Description | +|-------------|-----------------------------------------------------| +| `PUBLIC` | The resource is accessible to all users. | +| `READ_ONLY` | Users can view but not modify the resource. | +| `READ_WRITE` | Users can view and modify the resource. | + +By default, all resources are private and only visible to the owner and super-admins. Resources become accessible to others only when explicitly shared. + +SPI provides you an interface, with two default scopes `PUBLIC` and `RESTRICTED`, which can be extended to introduce more plugin-specific values. + +### **Using Scopes in API Design** +- APIs should be logically paired with correct scopes. + - Example, **GET APIs** should be logically paired with **`READ_ONLY`**, **`READ_WRITE`**, or **`PUBLIC`** scopes. When verifying access, these scopes must be **passed to the security plugin** via the `ResourceSharingNodeClient` to determine whether a user has the required permissions. + + +--- + +## **6. User Setup** + +To enable users to interact with the **Resource Sharing and Access Control** feature, they must be assigned the appropriate cluster permissions along with resource-specific access. + +### **Required Cluster Permissions** +Users must be assigned the following **cluster permissions** in `roles.yml`: + +- **`cluster:admin/security/resource_access`** → Required to evaluate resource permissions. +- **Plugin-specific cluster permissions** → Required to interact with the plugin’s APIs. + +#### **Example Role Configurations** +```yaml +sample_full_access: + cluster_permissions: + - 'cluster:admin/security/resource_access' + - 'cluster:admin/sample-resource-plugin/*' + +sample_read_access: + cluster_permissions: + - 'cluster:admin/security/resource_access' + - 'cluster:admin/sample-resource-plugin/get' +``` + + +### **User Access Rules** +1. **Users must have the required cluster permissions** + - Even if a resource is shared with a user, they **cannot access it** unless they have the **plugin’s cluster permissions**. + +2. **Granting plugin API permissions does not automatically grant resource access** + - A resource must be **explicitly shared** with the user. + - **Or, the user must be the resource owner.** + +3. **No index permissions are required** + - Access control is **handled at the cluster level**. + - The `.opensearch_resource_sharing` index and the resource indices are protected under system index security. + + +### **Summary** +| **Requirement** | **Description** | +|---------------|---------------------------------------------------------------------------------------| +| **Cluster Permission** | `cluster:admin/security/resource_access` required for resource evaluation. | +| **Plugin API Permissions** | Users must also have relevant plugin API cluster permissions. | +| **Resource Sharing** | Access is granted only if the resource is shared with the user or they are the owner. | +| **No Index Permissions Needed** | The `.opensearch_resource_sharing` index and resource indices are system-protected. | + + +--- + +## **7. Restrictions** +1. At present, **only resource owners can share/revoke access** to their own resources. + - **Super admins** can manage access for any resource. +2. **Resources must be stored in a system index**, and system index protection **must be enabled**. + - **Disabling system index protection** allows users to access resources **directly** if they have relevant index permissions. + +--- + +## **8. REST APIs Introduced by the Security Plugin** + +In addition to client methods, the **Security Plugin** introduces new **REST APIs** for managing resource access when the feature is enabled. These APIs allow users to **verify, grant, revoke, and list access** to resources. + +--- + +### **1. Verify Access** +- **Endpoint:** + ``` + POST /_plugins/_security/resources/verify_access + ``` +- **Description:** + Verifies whether the current user has access to a specified resource within the given index and scopes. + +#### **Request Body:** +```json +{ + "resource_id": "my-resource", + "resource_index": "resource-index", + "scopes": ["READ_ONLY"] +} +``` + +#### **Request Fields:** +| Field | Type | Description | +|-----------------|----------|-------------| +| `resource_id` | String | Unique identifier of the resource being accessed. | +| `resource_index`| String | The OpenSearch index where the resource is stored. | +| `scopes` | Array | The list of scopes to check access against (e.g., `"READ_ONLY"`, `"READ_WRITE"`). | + +#### **Response:** +Returns whether the user has permission to access the resource. +```json +{ + "has_permission": true +} +``` + +#### **Response Fields:** +| Field | Type | Description | +|-----------------|---------|-------------| +| `has_permission` | Boolean | `true` if the user has access, `false` otherwise. | + +--- + +### **2. Grant Access** +- **Endpoint:** + ``` + POST /_plugins/_security/resources/share + ``` +- **Description:** + Grants access to a resource for specified **users, roles, and backend roles** under defined **scopes**. + +#### **Request Body:** +```json +{ + "resource_id": "my-resource", + "resource_index": "resource-index", + "share_with": { + "your-scope-name": { + "users": ["shared-user-name"], + "backend_roles": ["shared-backend-roles"] + }, + "your-scope-name-2": { + "roles": ["shared-roles"] + } + } +} +``` + +#### **Request Fields:** +| Field | Type | Description | +|-----------------|---------|-------------| +| `resource_id` | String | The unique identifier of the resource to be shared. | +| `resource_index`| String | The OpenSearch index where the resource is stored. | +| `share_with` | Object | Defines which **users, roles, or backend roles** will gain access. | +| `your-scope-name` | Object | The scope under which the resource is shared (e.g., `"READ_ONLY"`, `"PUBLIC"`). | +| `users` | Array | List of usernames allowed to access the resource. | +| `roles` | Array | List of role names granted access. | +| `backend_roles`| Array | List of backend roles assigned to the resource. | + +#### **Response:** +Returns the updated **resource sharing state**. +```json +{ + "sharing_info": { + "source_idx": "resource-index", + "resource_id": "my-resource", + "created_by": { + "user": "you" + }, + "share_with": { + "your-scope-name": { + "users": ["shared-user-name"], + "backend_roles": ["shared-backend-roles"] + }, + "your-scope-name-2": { + "roles": ["shared-roles"] + } + } + } +} +``` + +#### **Response Fields:** +| Field | Type | Description | +|---------------|---------|-------------| +| `sharing_info` | Object | Contains information about how the resource is shared. | +| `source_idx` | String | The OpenSearch index containing the resource. | +| `resource_id` | String | The unique identifier of the resource being shared. | +| `created_by` | Object | Information about the user who created the sharing entry. | +| `share_with` | Object | Defines users, roles, and backend roles with access to the resource. | + +--- + +### **3. Revoke Access** +- **Endpoint:** + ``` + POST /_plugins/_security/resources/revoke + ``` +- **Description:** + Revokes access to a resource for specific users, roles, or backend roles under certain scopes. + +#### **Request Body:** +```json +{ + "resource_id": "my-resource", + "resource_index": "resource-index", + "entities_to_revoke": { + "roles": ["shared-roles"] + }, + "scopes": ["your-scope-name-2"] +} +``` + +#### **Request Fields:** +| Field | Type | Description | +|-----------------|---------|-------------| +| `resource_id` | String | The unique identifier of the resource whose access is being revoked. | +| `resource_index`| String | The OpenSearch index where the resource is stored. | +| `entities_to_revoke` | Object | Specifies which **users, roles, or backend roles** should have their access removed. | +| `roles` | Array | List of roles to revoke access from. | +| `scopes` | Array | List of scopes from which access should be revoked. | + +#### **Response:** +Returns the updated **resource sharing state** after revocation. +```json +{ + "sharing_info": { + "source_idx": "resource-index", + "resource_id": "my-resource", + "created_by": { + "user": "admin" + }, + "share_with": { + "your-scope-name": { + "users": ["shared-user-name"], + "backend_roles": ["shared-backend-roles"] + } + } + } +} +``` + +#### **Response Fields:** +| Field | Type | Description | +|---------------|---------|-------------| +| `sharing_info` | Object | Contains information about the updated resource sharing state. | +| `source_idx` | String | The OpenSearch index containing the resource. | +| `resource_id` | String | The unique identifier of the resource. | +| `created_by` | Object | Information about the user who created the sharing entry. | +| `share_with` | Object | Defines users, roles, and backend roles that still have access to the resource. | + +--- + +### **4. List Accessible Resources** +- **Endpoint:** + ``` + GET /_plugins/_security/resources/list/{resource_index} + ``` +- **Description:** + Retrieves a list of **resources that the current user has access to** within the specified `{resource_index}`. + +#### **Response:** +Returns an array of accessible resources. +```json +{ + "resources": [ + { + "name": "my-resource-name", + "description": "My resource description.", + "attributes": { + "type": "model" + } + } + ] +} +``` +*This is an example resource. Actual structure will vary based on your configuration.* + +--- + +## **Additional Notes** +- **Feature Flag:** These APIs are available only when `plugins.security.resource_sharing.enabled` is set to `true` in the configuration. +- **Index Restrictions:** Resources must be stored in **system indices**, and **system index protection** must be enabled to prevent unauthorized access. +- **Scopes Flexibility:** The `share_with` field allows defining **custom access scopes** as per plugin requirements. + +--- + +## **9. Best Practices** +### **For Plugin Developers** +- **Declare resources properly** in the `ResourceSharingExtension`. +- **Use the security client** instead of direct index queries to check access. +- **Implement a resource parser** to ensure correct resource extraction. + +### **For Users & Admins** +- **Keep system index protection enabled** for better security. +- **Grant access only when necessary** to limit exposure. + +--- + +## **Conclusion** +The **Resource Sharing and Access Control** feature enhances OpenSearch security by introducing an **additional layer of fine-grained access management** for plugin-defined resources. While **Fine-Grained Access Control (FGAC)** is already enabled, this feature provides **even more granular control** specifically for **resource-level access** within plugins. + +By implementing the **Service Provider Interface (SPI)**, utilizing the **security client**, and following **best practices**, developers can seamlessly integrate this feature into their plugins to enforce controlled resource sharing and access management. + +For detailed implementation and examples, refer to the **[sample plugin](./sample-resource-plugin/README.md)** included in the security plugin repository. + +--- + +## **License** +This project is licensed under the **Apache 2.0 License**. + +--- + +## **Copyright** +© OpenSearch Contributors. diff --git a/build.gradle b/build.gradle index 9d803c03f2..0b52259149 100644 --- a/build.gradle +++ b/build.gradle @@ -569,6 +569,8 @@ allprojects { 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') + integrationTestImplementation project(path: ":${rootProject.name}-common", configuration: 'shadow') + integrationTestImplementation project(path: ":${rootProject.name}-client", configuration: 'shadow') } } @@ -640,7 +642,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { - implementation project(path:":opensearch-resource-sharing-spi", configuration: 'shadow') + implementation project(path: ":${rootProject.name}-common", 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}" @@ -801,6 +803,7 @@ dependencies { exclude group: 'com.google.guava' } + testImplementation project(path: ":${rootProject.name}-common", configuration: 'shadow') } jar { diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000000..2f944adb35 --- /dev/null +++ b/client/README.md @@ -0,0 +1,233 @@ +Here's a **refined and corrected** version of your `README.md` file with improved clarity, grammar, and formatting: + +--- + +# **Resource Sharing Client** + +This package provides a **ResourceSharing client** that resource plugins can use to **implement access control** by communicating with the **OpenSearch Security Plugin**. + +--- + +## **Usage** + +### **1. Creating a Client Accessor with Singleton Pattern** +To ensure a single instance of the `ResourceSharingNodeClient`, use the **Singleton pattern**: + +```java +public class ResourceSharingClientAccessor { + private static ResourceSharingNodeClient INSTANCE; + + private ResourceSharingClientAccessor() {} + + /** + * Get the resource sharing client instance. + * + * @param nodeClient The OpenSearch NodeClient instance. + * @param settings The OpenSearch settings. + * @return A singleton instance of ResourceSharingNodeClient. + */ + public static ResourceSharingNodeClient getResourceSharingClient(NodeClient nodeClient, Settings settings) { + if (INSTANCE == null) { + INSTANCE = new ResourceSharingNodeClient(nodeClient, settings); + } + return INSTANCE; + } +} +``` + +--- + +### **2. Using the Client in a Transport Action** +The following example demonstrates how to use the **Resource Sharing Client** inside a `TransportAction` to verify **delete permissions** before deleting a resource. + +```java +@Override +protected void doExecute(Task task, DeleteResourceRequest request, ActionListener listener) { + String resourceId = request.getResourceId(); + + ResourceSharingClient resourceSharingClient = ResourceSharingClientAccessor.getResourceSharingClient(nodeClient, settings); + + resourceSharingClient.verifyResourceAccess( + resourceId, + RESOURCE_INDEX_NAME, + SampleResourceScope.PUBLIC.value(), + ActionListener.wrap(isAuthorized -> { + if (!isAuthorized) { + listener.onFailure(new ResourceSharingException("Current user is not authorized to delete resource: " + resourceId)); + return; + } + + // Authorization successful, proceed with deletion + ThreadContext threadContext = transportService.getThreadPool().getThreadContext(); + try (ThreadContext.StoredContext ignored = threadContext.stashContext()) { + deleteResource(resourceId, ActionListener.wrap(deleteResponse -> { + if (deleteResponse.getResult() == DocWriteResponse.Result.NOT_FOUND) { + listener.onFailure(new ResourceNotFoundException("Resource " + resourceId + " not found.")); + } else { + listener.onResponse(new DeleteResourceResponse("Resource " + resourceId + " deleted successfully.")); + } + }, exception -> { + log.error("Failed to delete resource: " + resourceId, exception); + listener.onFailure(exception); + })); + } + }, exception -> { + log.error("Failed to verify resource access: " + resourceId, exception); + listener.onFailure(exception); + }) + ); +} +``` + +--- + +## **Available Java APIs** + +The **`ResourceSharingClient`** provides **four Java APIs** for **resource access control**, enabling plugins to **verify, share, revoke, and list** resources. + +**Package Location:** +[`org.opensearch.security.client.resources.ResourceSharingClient`](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java) + +--- + +### **API Usage Examples** +Below are examples demonstrating how to use each API effectively. + +--- + +### **1. `verifyResourceAccess`** +**Checks if the current user has access to a resource** based on predefined **scopes**. + +#### **Method Signature:** +```java +void verifyResourceAccess(String resourceId, String resourceIndex, Set scopes, ActionListener listener); +``` + +#### **Example Usage:** +```java +Set scopes = Set.of("READ_ONLY"); +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()); + }) +); +``` +> **Use Case:** Before performing operations like **deletion or modifications**, ensure the user has the right permissions. + +--- + +### **2. `shareResource`** +**Grants access to a resource** for specific users, roles, or backend roles. + +#### **Method Signature:** +```java +void shareResource(String resourceId, String resourceIndex, Map shareWith, ActionListener listener); +``` + +#### **Example Usage:** +```java +Map shareWith = Map.of( + "users", List.of("user_1", "user_2"), + "roles", List.of("admin_role"), + "backend_roles", List.of("backend_group") +); + +resourceSharingClient.shareResource( + "resource-123", + "resource_index", + shareWith, + ActionListener.wrap(response -> { + System.out.println("Resource successfully shared with: " + shareWith); + }, e -> { + System.err.println("Failed to share resource: " + e.getMessage()); + }) +); +``` +> **Use Case:** Used when an **owner/admin wants to share a resource** with specific users or groups. + +--- + +### **3. `revokeResourceAccess`** +**Removes access permissions** for specified users, roles, or backend roles. + +#### **Method Signature:** +```java +void revokeResourceAccess(String resourceId, String resourceIndex, Map entitiesToRevoke, Set scopes, ActionListener listener); +``` + +#### **Example Usage:** +```java +Map entitiesToRevoke = Map.of( + "users", List.of("user_2"), + "roles", List.of("viewer_role") +); +Set scopesToRevoke = Set.of("READ_ONLY"); + +resourceSharingClient.revokeResourceAccess( + "resource-123", + "resource_index", + entitiesToRevoke, + scopesToRevoke, + ActionListener.wrap(response -> { + System.out.println("Resource access successfully revoked for: " + entitiesToRevoke); + }, e -> { + System.err.println("Failed to revoke access: " + e.getMessage()); + }) +); +``` +> **Use Case:** When a user no longer needs access to a **resource**, their permissions can be revoked. + +--- + +### **4. `listAllAccessibleResources`** +**Retrieves all resources the current user has access to.** + +#### **Method Signature:** +```java +void listAllAccessibleResources(String resourceIndex, ActionListener> listener); +``` + +#### **Example Usage:** +```java +resourceSharingClient.listAllAccessibleResources( + "resource_index", + ActionListener.wrap(resources -> { + for (Resource resource : resources) { + System.out.println("Accessible Resource: " + resource.getId()); + } + }, e -> { + System.err.println("Failed to list accessible resources: " + e.getMessage()); + }) +); +``` +> **Use Case:** Helps a user identify **which resources they can interact with**. + +--- + +## **Conclusion** +These APIs provide essential methods for **fine-grained resource access control**, enabling: + +✔ **Verification** of resource access. +✔ **Granting and revoking** access dynamically. +✔ **Retrieval** of all accessible resources. + +For further details, refer to the [`ResourceSharingClient` Java class](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java). + +--- + +## **License** +This project is licensed under the **Apache 2.0 License**. + +--- + +## **Copyright** +© OpenSearch Contributors. diff --git a/client/build.gradle b/client/build.gradle new file mode 100644 index 0000000000..8bef3910bc --- /dev/null +++ b/client/build.gradle @@ -0,0 +1,101 @@ +/* + * 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") + isSnapshot = "true" == System.getProperty("build.snapshot", "true") + buildVersionQualifier = System.getProperty("build.version_qualifier", "alpha1") + + // 2.0.0-rc1-SNAPSHOT -> 2.0.0.0-rc1-SNAPSHOT + version_tokens = opensearch_version.tokenize('-') + opensearch_build = version_tokens[0] + '.0' + + if (buildVersionQualifier) { + opensearch_build += "-${buildVersionQualifier}" + } + if (isSnapshot) { + opensearch_build += "-SNAPSHOT" + } +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +dependencies { + compileOnly "org.opensearch:opensearch:${opensearch_version}" + // SPI dependency comes through common + implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') +} + +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 Security Client") + packaging = "jar" + description.set("OpenSearch Security Client") + 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/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java new file mode 100644 index 0000000000..615f27ed68 --- /dev/null +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -0,0 +1,65 @@ +/* + * 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.client.resources; + +import java.util.Map; +import java.util.Set; + +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; + +/** + * Interface for resource sharing client operations. + * + * @opensearch.experimental + */ +public interface ResourceSharingClient { + + /** + * Verifies if the current user has access to the specified resource. + * @param resourceId The ID of the resource to verify access for. + * @param resourceIndex The index containing the resource. + * @param scopes The scopes to be checked against. + * @param listener The listener to be notified with the access verification result. + */ + void verifyResourceAccess(String resourceId, String resourceIndex, Set scopes, ActionListener listener); + + /** + * Shares a resource with the specified users, roles, and backend roles. + * @param resourceId The ID of the resource to share. + * @param resourceIndex The index containing the resource. + * @param shareWith The users, roles, and backend roles to share the resource with. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + void shareResource(String resourceId, String resourceIndex, Map shareWith, ActionListener listener); + + /** + * Revokes access to a resource for the specified entities and scopes. + * @param resourceId The ID of the resource to revoke access for. + * @param resourceIndex The index containing the resource. + * @param entitiesToRevoke The entities to revoke access for. + * @param scopes The scopes to revoke access for. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + void revokeResourceAccess( + String resourceId, + String resourceIndex, + Map entitiesToRevoke, + Set scopes, + ActionListener listener + ); + + /** + * Lists all resources accessible by the current user. + * @param resourceIndex The index containing the resources. + * @param listener The listener to be notified with the set of accessible resources. + */ + void listAllAccessibleResources(String resourceIndex, ActionListener> listener); +} diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java new file mode 100644 index 0000000000..239e23e128 --- /dev/null +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -0,0 +1,186 @@ +/* + * 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.client.resources; + +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.security.common.resources.rest.ResourceAccessAction; +import org.opensearch.security.common.resources.rest.ResourceAccessRequest; +import org.opensearch.security.common.resources.rest.ResourceAccessResponse; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.transport.client.Client; + +/** + * Client for resource sharing operations. + * + * @opensearch.experimental + */ +public final class ResourceSharingNodeClient implements ResourceSharingClient { + + private static final Logger log = LogManager.getLogger(ResourceSharingNodeClient.class); + + private final Client client; + private final boolean resourceSharingEnabled; + private final boolean isSecurityDisabled; + + public ResourceSharingNodeClient(Client client, Settings settings) { + this.client = client; + this.resourceSharingEnabled = settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + ); + this.isSecurityDisabled = settings.getAsBoolean( + ConfigConstants.OPENSEARCH_SECURITY_DISABLED, + ConfigConstants.OPENSEARCH_SECURITY_DISABLED_DEFAULT + ); + } + + /** + * Verifies if the current user has access to the specified resource. + * @param resourceId The ID of the resource to verify access for. + * @param resourceIndex The index containing the resource. + * @param scopes The scopes to be checked against. + * @param listener The listener to be notified with the access verification result. + */ + @Override + public void verifyResourceAccess(String resourceId, String resourceIndex, Set scopes, ActionListener listener) { + if (isSecurityDisabled || !resourceSharingEnabled) { + String message = isSecurityDisabled ? "Security Plugin is disabled." : "Resource Access Control feature is disabled."; + + log.warn("{} {}", message, "Access to resource is automatically granted"); + listener.onResponse(true); + return; + } + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.VERIFY) + .resourceId(resourceId) + .resourceIndex(resourceIndex) + .scopes(scopes) + .build(); + client.execute(ResourceAccessAction.INSTANCE, request, verifyAccessResponseListener(listener)); + } + + /** + * Shares the specified resource with the given users, roles, and backend roles. + * @param resourceId The ID of the resource to share. + * @param resourceIndex The index containing the resource. + * @param shareWith The users, roles, and backend roles to share the resource with. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + @Override + public void shareResource( + String resourceId, + String resourceIndex, + Map shareWith, + ActionListener listener + ) { + if (isResourceAccessControlOrSecurityPluginDisabled("Resource is not shareable.", listener)) { + return; + } + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.SHARE) + .resourceId(resourceId) + .resourceIndex(resourceIndex) + .shareWith(shareWith) + .build(); + client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); + } + + /** + * Revokes access to the specified resource for the given entities and scopes. + * @param resourceId The ID of the resource to revoke access for. + * @param resourceIndex The index containing the resource. + * @param entitiesToRevoke The entities to revoke access for. + * @param scopes The scopes to revoke access for. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + @Override + public void revokeResourceAccess( + String resourceId, + String resourceIndex, + Map entitiesToRevoke, + Set scopes, + ActionListener listener + ) { + if (isResourceAccessControlOrSecurityPluginDisabled("Resource access is not revoked.", listener)) { + return; + } + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.REVOKE) + .resourceId(resourceId) + .resourceIndex(resourceIndex) + .revokedEntities(entitiesToRevoke) + .scopes(scopes) + .build(); + client.execute(ResourceAccessAction.INSTANCE, request, sharingInfoResponseListener(listener)); + } + + /** + * Lists all resources accessible by the current user. + * + * @param listener The listener to be notified with the set of accessible resources. + */ + @Override + public void listAllAccessibleResources(String resourceIndex, ActionListener> listener) { + if (isResourceAccessControlOrSecurityPluginDisabled("Unable to list all accessible resources.", listener)) { + return; + } + ResourceAccessRequest request = new ResourceAccessRequest.Builder().operation(ResourceAccessRequest.Operation.LIST) + .resourceIndex(resourceIndex) + .build(); + client.execute( + ResourceAccessAction.INSTANCE, + request, + ActionListener.wrap(response -> { listener.onResponse(response.getResources()); }, listener::onFailure) + ); + } + + /** + * Checks if resource sharing or the security plugin is disabled and handles the error accordingly. + * + * @param disabledMessage The message to be logged if the feature is disabled. + * @param listener The listener to be notified with the error. + * @return {@code true} if either resource sharing or the security plugin is disabled, otherwise {@code false}. + */ + private boolean isResourceAccessControlOrSecurityPluginDisabled(String disabledMessage, ActionListener listener) { + if (isSecurityDisabled || !resourceSharingEnabled) { + String message = (isSecurityDisabled ? "Security Plugin" : "Resource Access Control feature") + " is disabled."; + + log.warn("{} {}", message, disabledMessage); + listener.onFailure(new ResourceSharingException(message + " " + disabledMessage, RestStatus.NOT_IMPLEMENTED)); + return true; + } + return false; + } + + /** + * Notifies the listener with the access request result. + * @param listener The listener to be notified with the access request result. + * @return An ActionListener that handles the ResourceAccessResponse and notifies the listener. + */ + private ActionListener verifyAccessResponseListener(ActionListener listener) { + return ActionListener.wrap(response -> listener.onResponse(response.getHasPermission()), listener::onFailure); + } + + /** + * Notifies the listener with the updated ResourceSharing document. + * @param listener The listener to be notified with the updated ResourceSharing document. + * @return An ActionListener that handles the ResourceAccessResponse and notifies the listener. + */ + private ActionListener sharingInfoResponseListener(ActionListener listener) { + return ActionListener.wrap(response -> listener.onResponse(response.getResourceSharing()), listener::onFailure); + } +} diff --git a/client/src/main/java/org/opensearch/security/client/resources/package-info.java b/client/src/main/java/org/opensearch/security/client/resources/package-info.java new file mode 100644 index 0000000000..1e15c4c46d --- /dev/null +++ b/client/src/main/java/org/opensearch/security/client/resources/package-info.java @@ -0,0 +1,14 @@ +/* + * 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 a resource sharing client that will be utilized by resource plugins to call security plugin's transport actions, which handle resource access + * + * @opensearch.experimental + */ +package org.opensearch.security.client.resources; diff --git a/common/build.gradle b/common/build.gradle new file mode 100644 index 0000000000..2b8e67add5 --- /dev/null +++ b/common/build.gradle @@ -0,0 +1,91 @@ +/* + * 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 "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" + implementation project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') + compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" + compileOnly 'com.password4j:password4j:1.8.2' + compileOnly "com.google.guava:guava:${guava_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 Security Common") + packaging = "jar" + description.set("OpenSearch Security Common") + 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/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java b/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java new file mode 100644 index 0000000000..7a2dc137a6 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java @@ -0,0 +1,298 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.common; + +import java.io.IOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.InjectableValues; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.exc.InvalidFormatException; +import com.fasterxml.jackson.databind.exc.MismatchedInputException; +import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import org.opensearch.SpecialPermission; + +class ConfigMapSerializer extends StdSerializer> { + private static final Set SENSITIVE_CONFIG_KEYS = Set.of("password"); + + @SuppressWarnings("unchecked") + public ConfigMapSerializer() { + // Pass Map.class to the superclass + super((Class>) (Class) Map.class); + } + + @Override + public void serialize(Map value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeStartObject(); + for (Map.Entry entry : value.entrySet()) { + if (SENSITIVE_CONFIG_KEYS.contains(entry.getKey())) { + gen.writeStringField(entry.getKey(), "******"); // Redact + } else { + gen.writeObjectField(entry.getKey(), entry.getValue()); + } + } + gen.writeEndObject(); + } +} + +public class DefaultObjectMapper { + public static final ObjectMapper objectMapper = new ObjectMapper(); + public final static ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); + private static final ObjectMapper defaulOmittingObjectMapper = new ObjectMapper(); + + static { + objectMapper.setSerializationInclusion(Include.NON_NULL); + // exclude sensitive information from the request body, + // if jackson cant parse the entity, e.g. passwords, hashes and so on, + // but provides which property is unknown + objectMapper.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + defaulOmittingObjectMapper.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + YAML_MAPPER.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); + // objectMapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + defaulOmittingObjectMapper.setSerializationInclusion(Include.NON_DEFAULT); + defaulOmittingObjectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + YAML_MAPPER.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); + } + + private DefaultObjectMapper() {} + + public static void inject(final InjectableValues.Std injectableValues) { + objectMapper.setInjectableValues(injectableValues); + YAML_MAPPER.setInjectableValues(injectableValues); + defaulOmittingObjectMapper.setInjectableValues(injectableValues); + } + + public static boolean getOrDefault(Map properties, String key, boolean defaultValue) throws JsonProcessingException { + Object value = properties.get(key); + if (value == null) { + return defaultValue; + } else if (value instanceof Boolean) { + return (boolean) value; + } else if (value instanceof String) { + String text = ((String) value).trim(); + if ("true".equals(text) || "True".equals(text)) { + return true; + } + if ("false".equals(text) || "False".equals(text)) { + return false; + } + throw InvalidFormatException.from( + null, + "Cannot deserialize value of type 'boolean' from String \"" + text + "\": only \"true\" or \"false\" recognized)", + null, + Boolean.class + ); + } + throw MismatchedInputException.from( + null, + Boolean.class, + "Cannot deserialize instance of 'boolean' out of '" + value + "' (Property: " + key + ")" + ); + } + + @SuppressWarnings("unchecked") + public static T getOrDefault(Map properties, String key, T defaultValue) { + T value = (T) properties.get(key); + return value != null ? value : defaultValue; + } + + @SuppressWarnings("removal") + public static T readTree(JsonNode node, Class clazz) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.treeToValue(node, clazz)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static T readValue(String string, Class clazz) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, clazz)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static JsonNode readTree(String string) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readTree(string)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static String writeValueAsString(Object value, boolean omitDefaults) throws JsonProcessingException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction) () -> (omitDefaults ? defaulOmittingObjectMapper : objectMapper).writeValueAsString( + value + ) + ); + } catch (final PrivilegedActionException e) { + throw (JsonProcessingException) e.getCause(); + } + + } + + @SuppressWarnings("removal") + public static String writeValueAsStringAndRedactSensitive(Object value) throws JsonProcessingException { + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + SimpleModule module = new SimpleModule(); + module.addSerializer(new ConfigMapSerializer()); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(module); + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> mapper.writeValueAsString(value)); + } catch (final PrivilegedActionException e) { + throw (JsonProcessingException) e.getCause(); + } + + } + + @SuppressWarnings("removal") + public static T readValue(String string, TypeReference tr) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public T run() throws Exception { + return objectMapper.readValue(string, tr); + } + }); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + + } + + @SuppressWarnings("removal") + public static T readValue(String string, JavaType jt) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, jt)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + @SuppressWarnings("removal") + public static T convertValue(JsonNode jsonNode, JavaType jt) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.convertValue(jsonNode, jt)); + } catch (final PrivilegedActionException e) { + throw (IOException) e.getCause(); + } + } + + public static TypeFactory getTypeFactory() { + return objectMapper.getTypeFactory(); + } + + public static Set getFields(Class cls) { + return objectMapper.getSerializationConfig() + .introspect(getTypeFactory().constructType(cls)) + .findProperties() + .stream() + .map(BeanPropertyDefinition::getName) + .collect(ImmutableSet.toImmutableSet()); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java b/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java new file mode 100644 index 0000000000..3526404bbd --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java @@ -0,0 +1,40 @@ +/* + * 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.common.auditlog.impl; + +import java.util.Collection; +import java.util.Collections; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; + +public enum AuditCategory { + BAD_HEADERS, + FAILED_LOGIN, + MISSING_PRIVILEGES, + GRANTED_PRIVILEGES, + OPENDISTRO_SECURITY_INDEX_ATTEMPT, + SSL_EXCEPTION, + AUTHENTICATED, + INDEX_EVENT, + COMPLIANCE_DOC_READ, + COMPLIANCE_DOC_WRITE, + COMPLIANCE_EXTERNAL_CONFIG, + COMPLIANCE_INTERNAL_CONFIG_READ, + COMPLIANCE_INTERNAL_CONFIG_WRITE; + + public static Set parse(final Collection categories) { + if (categories.isEmpty()) return Collections.emptySet(); + + return categories.stream().map(String::toUpperCase).map(AuditCategory::valueOf).collect(ImmutableSet.toImmutableSet()); + } +} diff --git a/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java b/common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java similarity index 83% rename from src/main/java/org/opensearch/security/auth/UserSubjectImpl.java rename to common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java index 63adc559e3..620250be53 100644 --- a/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java +++ b/common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java @@ -7,7 +7,7 @@ * compatible open source license. * */ -package org.opensearch.security.auth; +package org.opensearch.security.common.auth; import java.security.Principal; import java.util.concurrent.Callable; @@ -16,8 +16,8 @@ import org.opensearch.identity.NamedPrincipal; import org.opensearch.identity.UserSubject; import org.opensearch.identity.tokens.AuthToken; -import org.opensearch.security.support.ConfigConstants; -import org.opensearch.security.user.User; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.user.User; import org.opensearch.threadpool.ThreadPool; public class UserSubjectImpl implements UserSubject { @@ -25,7 +25,7 @@ public class UserSubjectImpl implements UserSubject { private final ThreadPool threadPool; private final User user; - UserSubjectImpl(ThreadPool threadPool, User user) { + public UserSubjectImpl(ThreadPool threadPool, User user) { this.threadPool = threadPool; this.user = user; this.userPrincipal = new NamedPrincipal(user.getName()); @@ -48,4 +48,8 @@ public T runAs(Callable callable) throws Exception { return callable.call(); } } + + public User getUser() { + return user; + } } diff --git a/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java b/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java new file mode 100644 index 0000000000..22647e6685 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java @@ -0,0 +1,162 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.common.configuration; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.function.Function; +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; + +import com.google.common.collect.ImmutableMap; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.support.WildcardMatcher; +import org.opensearch.security.common.user.User; + +public class AdminDNs { + + protected final Logger log = LogManager.getLogger(AdminDNs.class); + private final Set adminDn = new HashSet(); + private final Set adminUsernames = new HashSet(); + private final Map allowedDnsImpersonations; + private final Map allowedRestImpersonations; + private boolean injectUserEnabled; + private boolean injectAdminUserEnabled; + + public AdminDNs(final Settings settings) { + + this.injectUserEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false); + this.injectAdminUserEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, false); + + final List adminDnsA = settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList()); + + for (String dn : adminDnsA) { + try { + log.debug("{} is registered as an admin dn", dn); + adminDn.add(new LdapName(dn)); + } catch (final InvalidNameException e) { + // make sure to log correctly depending on user injection settings + if (injectUserEnabled && injectAdminUserEnabled) { + if (log.isDebugEnabled()) { + log.debug("Admin DN not an LDAP name, but admin user injection enabled. Will add {} to admin usernames", dn); + } + adminUsernames.add(dn); + } else { + log.error("Unable to parse admin dn {}", dn, e); + } + } + } + + log.debug("Loaded {} admin DN's {}", adminDn.size(), adminDn); + + final Settings impersonationDns = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + "."); + + allowedDnsImpersonations = impersonationDns.keySet() + .stream() + .map(this::toLdapName) + .filter(Objects::nonNull) + .collect( + ImmutableMap.toImmutableMap( + Function.identity(), + ldapName -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + "." + ldapName)) + ) + ); + + log.debug("Loaded {} impersonation DN's {}", allowedDnsImpersonations.size(), allowedDnsImpersonations); + + final Settings impersonationUsersRest = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "."); + + allowedRestImpersonations = impersonationUsersRest.keySet() + .stream() + .collect( + ImmutableMap.toImmutableMap( + Function.identity(), + user -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + user)) + ) + ); + + log.debug("Loaded {} impersonation users for REST {}", allowedRestImpersonations.size(), allowedRestImpersonations); + } + + private LdapName toLdapName(String dn) { + try { + return new LdapName(dn); + } catch (final InvalidNameException e) { + log.error("Unable to parse allowedImpersonations dn {}", dn, e); + } + return null; + } + + public boolean isAdmin(User user) { + if (isAdminDN(user.getName())) { + return true; + } + + // ThreadContext injected user, may be admin user, only if both flags are enabled and user is injected + if (injectUserEnabled && injectAdminUserEnabled && user.isInjected() && adminUsernames.contains(user.getName())) { + return true; + } + return false; + } + + public boolean isAdminDN(String dn) { + + if (dn == null) return false; + + try { + return isAdminDN(new LdapName(dn)); + } catch (InvalidNameException e) { + return false; + } + } + + private boolean isAdminDN(LdapName dn) { + if (dn == null) return false; + + boolean isAdmin = adminDn.contains(dn); + + if (log.isTraceEnabled()) { + log.trace("Is principal {} an admin cert? {}", dn.toString(), isAdmin); + } + + return isAdmin; + } + + public boolean isRestImpersonationAllowed(final String originalUser, final String impersonated) { + return (originalUser != null) + ? allowedRestImpersonations.getOrDefault(originalUser, WildcardMatcher.NONE).test(impersonated) + : false; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java b/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java new file mode 100644 index 0000000000..e2258e9e6e --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java @@ -0,0 +1,106 @@ +/* + * 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.common.dlic.rest.api; + +import java.io.IOException; + +import org.opensearch.ExceptionsHelper; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; + +public class Responses { + + public static void ok(final RestChannel channel, final String message) { + response(channel, RestStatus.OK, message); + } + + public static void ok(final RestChannel channel, final ToXContent toXContent) { + response(channel, RestStatus.OK, toXContent); + } + + public static void created(final RestChannel channel, final String message) { + response(channel, RestStatus.CREATED, message); + } + + public static void methodNotImplemented(final RestChannel channel, final RestRequest.Method method) { + notImplemented(channel, "Method " + method.name() + " not supported for this action."); + } + + public static void notImplemented(final RestChannel channel, final String message) { + response(channel, RestStatus.NOT_IMPLEMENTED, message); + } + + public static void notFound(final RestChannel channel, final String message) { + response(channel, RestStatus.NOT_FOUND, message); + } + + public static void conflict(final RestChannel channel, final String message) { + response(channel, RestStatus.CONFLICT, message); + } + + public static void internalServerError(final RestChannel channel, final String message) { + response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); + } + + public static void forbidden(final RestChannel channel, final String message) { + response(channel, RestStatus.FORBIDDEN, message); + } + + public static void badRequest(final RestChannel channel, final String message) { + response(channel, RestStatus.BAD_REQUEST, message); + } + + public static void unauthorized(final RestChannel channel) { + response(channel, RestStatus.UNAUTHORIZED, "Unauthorized"); + } + + public static void response(RestChannel channel, RestStatus status, String message) { + response(channel, status, payload(status, message)); + } + + public static void response(final RestChannel channel, final RestStatus status, final ToXContent toXContent) { + try (final var builder = channel.newBuilder()) { + toXContent.toXContent(builder, ToXContent.EMPTY_PARAMS); + channel.sendResponse(new BytesRestResponse(status, builder)); + } catch (final IOException ioe) { + throw ExceptionsHelper.convertToOpenSearchException(ioe); + } + } + + public static ToXContent forbiddenMessage(final String message) { + return payload(RestStatus.FORBIDDEN, message); + } + + public static ToXContent badRequestMessage(final String message) { + return payload(RestStatus.BAD_REQUEST, message); + } + + public static ToXContent methodNotImplementedMessage(final RestRequest.Method method) { + return payload(RestStatus.NOT_FOUND, "Method " + method.name() + " not supported for this action."); + } + + public static ToXContent notFoundMessage(final String message) { + return payload(RestStatus.NOT_FOUND, message); + } + + public static ToXContent conflictMessage(final String message) { + return payload(RestStatus.CONFLICT, message); + } + + public static ToXContent payload(final RestStatus status, final String message) { + return (builder, params) -> builder.startObject().field("status", status.name()).field("message", message).endObject(); + } + +} diff --git a/common/src/main/java/org/opensearch/security/common/package-info.java b/common/src/main/java/org/opensearch/security/common/package-info.java new file mode 100644 index 0000000000..01e2ead134 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/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 common classes required to implement resource access control in OpenSearch. + * TODO: At present it contains multiple duplicates, which will be address in a fast follow PR. + * + * @opensearch.experimental + */ +package org.opensearch.security.common; diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java new file mode 100644 index 0000000000..3912001aa1 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java @@ -0,0 +1,549 @@ +/* + * 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.common.resources; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.StepListener; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.security.common.auth.UserSubjectImpl; +import org.opensearch.security.common.configuration.AdminDNs; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.user.User; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; +import org.opensearch.security.spi.resources.sharing.Recipient; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.security.spi.resources.sharing.ShareWith; +import org.opensearch.security.spi.resources.sharing.SharedWithScope; +import org.opensearch.threadpool.ThreadPool; + +/** + * This class handles resource access permissions for users, roles and backend-roles. + * It provides methods to check if a user has permission to access a resource + * based on the resource sharing configuration. + * + * @opensearch.experimental + */ +public class ResourceAccessHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceAccessHandler.class); + + private final ThreadContext threadContext; + private final ResourceSharingIndexHandler resourceSharingIndexHandler; + private final AdminDNs adminDNs; + + public ResourceAccessHandler( + final ThreadPool threadPool, + final ResourceSharingIndexHandler resourceSharingIndexHandler, + AdminDNs adminDns + ) { + this.threadContext = threadPool.getThreadContext(); + this.resourceSharingIndexHandler = resourceSharingIndexHandler; + this.adminDNs = adminDns; + } + + /** + * Initializes the recipient types for users, roles, and backend roles. + * These recipient types are used to identify the types of recipients for resource sharing. + */ + public void initializeRecipientTypes() { + RecipientTypeRegistry.registerRecipientType(Recipient.USERS.getName(), new RecipientType(Recipient.USERS.getName())); + RecipientTypeRegistry.registerRecipientType(Recipient.ROLES.getName(), new RecipientType(Recipient.ROLES.getName())); + RecipientTypeRegistry.registerRecipientType( + Recipient.BACKEND_ROLES.getName(), + new RecipientType(Recipient.BACKEND_ROLES.getName()) + ); + } + + /** + * Returns a set of accessible resource IDs for the current user within the specified resource index. + * + * @param resourceIndex The resource index to check for accessible resources. + * @param listener The listener to be notified with the set of accessible resource IDs. + */ + public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionListener> listener) { + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + // If no user is authenticated, return an empty set + if (user == null) { + LOGGER.warn("Unable to fetch user details. User is null."); + listener.onResponse(Collections.emptySet()); + return; + } + + LOGGER.debug("Listing accessible resources within the resource index {} for user: {}", resourceIndex, user.getName()); + + // 2. If the user is admin, simply fetch all resources + if (adminDNs.isAdmin(user)) { + loadAllResources(resourceIndex, ActionListener.wrap(listener::onResponse, listener::onFailure)); + return; + } + + // StepListener for the user’s "own" resources + StepListener> ownResourcesListener = new StepListener<>(); + + // StepListener for resources shared with the user’s name + StepListener> userNameResourcesListener = new StepListener<>(); + + // StepListener for resources shared with the user’s roles + StepListener> rolesResourcesListener = new StepListener<>(); + + // StepListener for resources shared with the user’s backend roles + StepListener> backendRolesResourcesListener = new StepListener<>(); + + // Load own resources for the user. + loadOwnResources(resourceIndex, user.getName(), ownResourcesListener); + + // Load resources shared with the user by its name. + ownResourcesListener.whenComplete( + ownResources -> loadSharedWithResources( + resourceIndex, + Set.of(user.getName()), + Recipient.USERS.getName(), + userNameResourcesListener + ), + listener::onFailure + ); + + // Load resources shared with the user’s roles. + userNameResourcesListener.whenComplete( + userNameResources -> loadSharedWithResources( + resourceIndex, + user.getSecurityRoles(), + Recipient.ROLES.getName(), + rolesResourcesListener + ), + listener::onFailure + ); + + // Load resources shared with the user’s backend roles. + rolesResourcesListener.whenComplete( + rolesResources -> loadSharedWithResources( + resourceIndex, + user.getRoles(), + Recipient.BACKEND_ROLES.getName(), + backendRolesResourcesListener + ), + listener::onFailure + ); + + // Combine all results and pass them back to the original listener. + backendRolesResourcesListener.whenComplete(backendRolesResources -> { + Set allResources = new HashSet<>(); + + // Retrieve results from each StepListener + allResources.addAll(ownResourcesListener.result()); + allResources.addAll(userNameResourcesListener.result()); + allResources.addAll(rolesResourcesListener.result()); + allResources.addAll(backendRolesResourcesListener.result()); + + LOGGER.debug("Found {} accessible resources for user {}", allResources.size(), user.getName()); + listener.onResponse(allResources); + }, listener::onFailure); + } + + /** + * Returns a set of accessible resources for the current user within the specified resource index. + * + * @param resourceIndex The resource index to check for accessible resources. + * @param listener The listener to be notified with the set of accessible resources. + */ + @SuppressWarnings("unchecked") + public void getAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { + try { + validateArguments(resourceIndex); + + ResourceParser parser = ResourcePluginInfo.getInstance().getResourceProviders().get(resourceIndex).resourceParser(); + + StepListener> resourceIdsListener = new StepListener<>(); + StepListener> resourcesListener = new StepListener<>(); + + // Fetch resource IDs + getAccessibleResourceIdsForCurrentUser(resourceIndex, resourceIdsListener); + + // Fetch docs + resourceIdsListener.whenComplete(resourceIds -> { + if (resourceIds.isEmpty()) { + // No accessible resources => immediately respond with empty set + listener.onResponse(Collections.emptySet()); + } else { + // Fetch the resource documents asynchronously + this.resourceSharingIndexHandler.getResourceDocumentsFromIds(resourceIds, resourceIndex, parser, resourcesListener); + } + }, listener::onFailure); + + // Send final response + resourcesListener.whenComplete( + listener::onResponse, + ex -> listener.onFailure(new ResourceSharingException("Failed to get accessible resources: " + ex.getMessage(), ex)) + ); + } catch (Exception e) { + LOGGER.warn("Failed to process accessible resources request: {}", e.getMessage()); + listener.onFailure(new ResourceSharingException("Failed to process accessible resources request: " + e.getMessage(), e)); + } + } + + /** + * Checks whether current user has given permission (scope) to access given resource. + * + * @param resourceId The resource ID to check access for. + * @param resourceIndex The resource index containing the resource. + * @param scopes The permission scope(s) to check. + * @param listener The listener to be notified with the permission check result. + */ + public void hasPermission(String resourceId, String resourceIndex, Set scopes, ActionListener listener) { + validateArguments(resourceId, resourceIndex, scopes); + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + LOGGER.warn("No authenticated user found. Access to resource {} is not authorized", resourceId); + listener.onResponse(false); + return; + } + + LOGGER.debug("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); + + if (adminDNs.isAdmin(user)) { + LOGGER.debug( + "User '{}' is admin, automatically granted '{}' permission on '{}'", + user.getName(), + scopes.toString(), + resourceId + ); + listener.onResponse(true); + return; + } + + Set userRoles = new HashSet<>(user.getSecurityRoles()); + Set userBackendRoles = new HashSet<>(user.getRoles()); + + this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> { + if (document == null) { + LOGGER.warn("Resource '{}' not found in index '{}'", resourceId, resourceIndex); + listener.onFailure(new ResourceSharingException("Resource " + resourceId + " not found in index " + resourceIndex)); + return; + } + + // All public entities are designated with "*" + userRoles.add("*"); + userBackendRoles.add("*"); + if (isOwnerOfResource(document, user.getName()) + || isSharedWithEveryone(document) + || isSharedWithEntity(document, Recipient.USERS, Set.of(user.getName(), "*"), scopes) + || isSharedWithEntity(document, Recipient.ROLES, userRoles, scopes) + || isSharedWithEntity(document, Recipient.BACKEND_ROLES, userBackendRoles, scopes)) { + + LOGGER.debug("User '{}' has '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); + listener.onResponse(true); + } else { + LOGGER.debug("User '{}' does not have '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); + listener.onResponse(false); + } + }, exception -> { + LOGGER.error( + "Failed to fetch resource sharing document for resource '{}' in index '{}': {}", + resourceId, + resourceIndex, + exception.getMessage() + ); + listener.onFailure(exception); + })); + } + + /** + * Shares a resource with the specified users, roles, and backend roles. + * + * @param resourceId The resource ID to share. + * @param resourceIndex The index where resource is store + * @param shareWith The users, roles, and backend roles as well as scope to share the resource with. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + public void shareWith(String resourceId, String resourceIndex, ShareWith shareWith, ActionListener listener) { + validateArguments(resourceId, resourceIndex, shareWith); + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + LOGGER.warn("No authenticated user found. Failed to share resource {}", resourceId); + listener.onFailure(new ResourceSharingException("No authenticated user found. Failed to share resource " + resourceId)); + return; + } + + LOGGER.debug("Sharing resource {} created by {} with {}", resourceId, user.getName(), shareWith.toString()); + + boolean isAdmin = adminDNs.isAdmin(user); + + this.resourceSharingIndexHandler.updateResourceSharingInfo( + resourceId, + resourceIndex, + user.getName(), + shareWith, + isAdmin, + ActionListener.wrap(updatedResourceSharing -> { + LOGGER.debug("Successfully shared resource {} with {}", resourceId, shareWith.toString()); + listener.onResponse(updatedResourceSharing); + }, e -> { + LOGGER.error("Failed to share resource {} with {}: {}", resourceId, shareWith.toString(), e.getMessage()); + listener.onFailure(e); + }) + ); + } + + /** + * Revokes access to a resource for the specified users, roles, and backend roles. + * + * @param resourceId The resource ID to revoke access from. + * @param resourceIndex The index where resource is store + * @param revokeAccess The users, roles, and backend roles to revoke access for. + * @param scopes The permission scopes to revoke access for. + * @param listener The listener to be notified with the updated ResourceSharing document. + */ + public void revokeAccess( + String resourceId, + String resourceIndex, + Map> revokeAccess, + Set scopes, + ActionListener listener + ) { + validateArguments(resourceId, resourceIndex, revokeAccess, scopes); + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + LOGGER.warn("No authenticated user found. Failed to revoker access to resource {}", resourceId); + listener.onFailure( + new ResourceSharingException("No authenticated user found. Failed to revoke access to resource {}" + resourceId) + ); + return; + } + + LOGGER.debug("User {} revoking access to resource {} for {} for scopes {}.", user.getName(), resourceId, revokeAccess, scopes); + + boolean isAdmin = adminDNs.isAdmin(user); + + this.resourceSharingIndexHandler.revokeAccess( + resourceId, + resourceIndex, + revokeAccess, + scopes, + user.getName(), + isAdmin, + ActionListener.wrap(listener::onResponse, exception -> { + LOGGER.error("Failed to revoke access to resource {} in index {}: {}", resourceId, resourceIndex, exception.getMessage()); + listener.onFailure(exception); + }) + ); + } + + /** + * Deletes a resource sharing record by its ID and the resource index it belongs to. + * + * @param resourceId The resource ID to delete. + * @param resourceIndex The resource index containing the resource. + * @param listener The listener to be notified with the deletion result. + */ + public void deleteResourceSharingRecord(String resourceId, String resourceIndex, ActionListener listener) { + try { + validateArguments(resourceId, resourceIndex); + + LOGGER.debug("Deleting resource sharing record for resource {} in {}", resourceId, resourceIndex); + + StepListener deleteDocListener = new StepListener<>(); + resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, deleteDocListener); + deleteDocListener.whenComplete(listener::onResponse, listener::onFailure); + + } catch (Exception e) { + LOGGER.error("Failed to delete resource sharing record for resource {}", resourceId, e); + listener.onFailure(e); + } + } + + /** + * Deletes all resource sharing records for the current user. + * + * @param listener The listener to be notified with the deletion result. + */ + public void deleteAllResourceSharingRecordsForCurrentUser(ActionListener listener) { + final UserSubjectImpl userSubject = (UserSubjectImpl) threadContext.getPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER + ); + final User user = (userSubject == null) ? null : userSubject.getUser(); + + if (user == null) { + listener.onFailure(new ResourceSharingException("No authenticated user available.")); + return; + } + + LOGGER.debug("Deleting all resource sharing records for user {}", user.getName()); + + resourceSharingIndexHandler.deleteAllRecordsForUser(user.getName(), ActionListener.wrap(listener::onResponse, exception -> { + LOGGER.error( + "Failed to delete all resource sharing records for user {}: {}", + user.getName(), + exception.getMessage(), + exception + ); + listener.onFailure(exception); + })); + } + + /** + * Loads all resources within the specified resource index. + * + * @param resourceIndex The resource index to load resources from. + * @param listener The listener to be notified with the set of resource IDs. + */ + private void loadAllResources(String resourceIndex, ActionListener> listener) { + this.resourceSharingIndexHandler.fetchAllDocuments(resourceIndex, listener); + } + + /** + * Loads resources owned by the specified user within the given resource index. + * + * @param resourceIndex The resource index to load resources from. + * @param userName The username of the owner. + * @param listener The listener to be notified with the set of resource IDs. + */ + private void loadOwnResources(String resourceIndex, String userName, ActionListener> listener) { + this.resourceSharingIndexHandler.fetchDocumentsByField(resourceIndex, "created_by.user", userName, listener); + } + + /** + * Loads resources shared with the specified entities within the given resource index, including public resources. + * + * @param resourceIndex The resource index to load resources from. + * @param entities The set of entities to check for shared resources. + * @param recipientType The type of entity (e.g., users, roles, backend_roles). + * @param listener The listener to be notified with the set of resource IDs. + */ + private void loadSharedWithResources( + String resourceIndex, + Set entities, + String recipientType, + ActionListener> listener + ) { + Set entitiesCopy = new HashSet<>(entities); + // To allow "public" resources to be matched for any user, role, backend_role + entitiesCopy.add("*"); + this.resourceSharingIndexHandler.fetchDocumentsForAllScopes(resourceIndex, entitiesCopy, recipientType, listener); + } + + /** + * Checks if the given resource is owned by the specified user. + * + * @param document The ResourceSharing document to check. + * @param userName The username to check ownership against. + * @return True if the resource is owned by the user, false otherwise. + */ + private boolean isOwnerOfResource(ResourceSharing document, String userName) { + return document.getCreatedBy() != null && document.getCreatedBy().getCreator().equals(userName); + } + + /** + * Checks if the given resource is shared with the specified entities and scope. + * + * @param document The ResourceSharing document to check. + * @param recipient The recipient entity + * @param entities The set of entities to check for sharing. + * @param scopes The permission scope(s) to check. + * @return True if the resource is shared with the entities and scope, false otherwise. + */ + private boolean isSharedWithEntity(ResourceSharing document, Recipient recipient, Set entities, Set scopes) { + for (String entity : entities) { + if (checkSharing(document, recipient, entity, scopes)) { + return true; + } + } + return false; + } + + /** + * Checks if the given resource is shared with everyone, i.e. the scope is named "*" + * + * @param document The ResourceSharing document to check. + * @return True if the resource is shared with everyone, false otherwise. + */ + private boolean isSharedWithEveryone(ResourceSharing document) { + return document.getShareWith() != null + && document.getShareWith().getSharedWithScopes().stream().anyMatch(sharedWithScope -> sharedWithScope.getScope().equals("*")); + } + + /** + * Checks if the given resource is shared with the specified entity and scope. + * + * @param document The ResourceSharing document to check. + * @param recipient The recipient entity + * @param entity The entity to check for sharing. + * @param scopes The permission scope(s) to check. + * @return True if the resource is shared with the entity and scope, false otherwise. + */ + private boolean checkSharing(ResourceSharing document, Recipient recipient, String entity, Set scopes) { + if (document.getShareWith() == null) { + return false; + } + + return document.getShareWith() + .getSharedWithScopes() + .stream() + .filter(sharedWithScope -> scopes.contains(sharedWithScope.getScope())) + .findFirst() + .map(sharedWithScope -> { + SharedWithScope.ScopeRecipients scopePermissions = sharedWithScope.getSharedWithPerScope(); + Map> recipients = scopePermissions.getRecipients(); + + return switch (recipient) { + case Recipient.USERS, Recipient.ROLES, Recipient.BACKEND_ROLES -> recipients.get( + RecipientTypeRegistry.fromValue(recipient.getName()) + ).contains(entity); + }; + }) + .orElse(false); // Return false if no matching scope is found + } + + private void validateArguments(Object... args) { + if (args == null) { + throw new IllegalArgumentException("Arguments cannot be null"); + } + for (Object arg : args) { + if (arg == null) { + throw new IllegalArgumentException("Argument cannot be null"); + } + // Additional check for String type arguments + if (arg instanceof String && ((String) arg).trim().isEmpty()) { + throw new IllegalArgumentException("Arguments cannot be empty"); + } + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java new file mode 100644 index 0000000000..4b502096b4 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java @@ -0,0 +1,123 @@ +/* + * 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.common.resources; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.index.shard.ShardId; +import org.opensearch.index.engine.Engine; +import org.opensearch.index.shard.IndexingOperationListener; +import org.opensearch.security.common.auth.UserSubjectImpl; +import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.common.user.User; +import org.opensearch.security.spi.resources.sharing.CreatedBy; +import org.opensearch.security.spi.resources.sharing.Creator; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; + +/** + * This class implements an index operation listener for operations performed on resources stored in plugin's indices. + * + * @opensearch.experimental + */ +public class ResourceIndexListener implements IndexingOperationListener { + + private static final Logger log = LogManager.getLogger(ResourceIndexListener.class); + private static final ResourceIndexListener INSTANCE = new ResourceIndexListener(); + private ResourceSharingIndexHandler resourceSharingIndexHandler; + + private boolean initialized; + private ThreadPool threadPool; + + private ResourceIndexListener() {} + + public static ResourceIndexListener getInstance() { + return ResourceIndexListener.INSTANCE; + } + + public void initialize(ThreadPool threadPool, Client client) { + if (initialized) { + return; + } + initialized = true; + this.threadPool = threadPool; + this.resourceSharingIndexHandler = new ResourceSharingIndexHandler( + ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, + client, + threadPool + ); + } + + public boolean isInitialized() { + return initialized; + } + + /** + * Creates a resource sharing entry for the newly created resource. + */ + @Override + public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + String resourceIndex = shardId.getIndexName(); + log.debug("postIndex called on {}", resourceIndex); + + String resourceId = index.id(); + + // Only proceed if this was a create operation + if (!result.isCreated()) { + log.debug("Skipping resource sharing entry creation as this was an update operation for resource {}", resourceId); + return; + } + + final UserSubjectImpl userSubject = (UserSubjectImpl) threadPool.getThreadContext() + .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); + final User user = userSubject.getUser(); + + try { + Objects.requireNonNull(user); + ResourceSharing sharing = this.resourceSharingIndexHandler.indexResourceSharing( + resourceId, + resourceIndex, + new CreatedBy(Creator.USER, user.getName()), + null + ); + log.debug( + "Successfully created a resource sharing entry {} for resource {} within index {}", + sharing, + resourceId, + resourceIndex + ); + } catch (IOException e) { + log.debug("Failed to create a resource sharing entry for resource: {}", resourceId, e); + } + } + + /** + * Deletes the resource sharing entry for the deleted resource. + */ + @Override + public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { + String resourceIndex = shardId.getIndexName(); + log.debug("postDelete called on {}", resourceIndex); + + String resourceId = delete.id(); + this.resourceSharingIndexHandler.deleteResourceSharingRecord(resourceId, resourceIndex, ActionListener.wrap(deleted -> { + if (deleted) { + log.debug("Successfully deleted resource sharing entry for resource {}", resourceId); + } else { + log.debug("No resource sharing entry found for resource {}", resourceId); + } + }, exception -> log.error("Failed to delete resource sharing entry for resource {}", resourceId, exception))); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java new file mode 100644 index 0000000000..fde006c198 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java @@ -0,0 +1,58 @@ +package org.opensearch.security.common.resources; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; + +/** + * This class provides information about resource plugins and their associated resource providers and indices. + * It follows the Singleton pattern to ensure that only one instance of the class exists. + * + * @opensearch.experimental + */ +public class ResourcePluginInfo { + private static ResourcePluginInfo INSTANCE; + + private final Map resourceProviderMap = new HashMap<>(); + private final Set resourceIndices = new HashSet<>(); + + private ResourcePluginInfo() {} + + public static ResourcePluginInfo getInstance() { + if (INSTANCE == null) { + INSTANCE = new ResourcePluginInfo(); + } + return INSTANCE; + } + + public void setResourceProviders(Map providerMap) { + resourceProviderMap.clear(); + resourceProviderMap.putAll(providerMap); + } + + public void setResourceIndices(Set indices) { + resourceIndices.clear(); + resourceIndices.addAll(indices); + } + + public Map getResourceProviders() { + return ImmutableMap.copyOf(resourceProviderMap); + } + + public Set getResourceIndices() { + return ImmutableSet.copyOf(resourceIndices); + } + + // TODO following should be removed once core test framework allows loading extended classes + public Map getResourceProvidersMutable() { + return resourceProviderMap; + } + + public Set getResourceIndicesMutable() { + return resourceIndices; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java new file mode 100644 index 0000000000..b2537fc849 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java @@ -0,0 +1,21 @@ +/* + * 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.common.resources; + +import org.opensearch.security.spi.resources.ResourceParser; + +/** + * This record class represents a resource provider. + * It holds information about the resource type, resource index name, and a resource parser. + * + * @opensearch.experimental + */ +public record ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { + +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java new file mode 100644 index 0000000000..a1004566e5 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java @@ -0,0 +1,21 @@ +/* + * 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.common.resources; + +/** + * This class contains constants related to resource sharing in OpenSearch. + * + * @opensearch.experimental + */ +public class ResourceSharingConstants { + // Resource sharing index + public static final String OPENSEARCH_RESOURCE_SHARING_INDEX = ".opensearch_resource_sharing"; +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java new file mode 100644 index 0000000000..8ff771d74e --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java @@ -0,0 +1,1402 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + */ +package org.opensearch.security.common.resources; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Callable; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.apache.commons.lang3.StringUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.DocWriteRequest; +import org.opensearch.action.StepListener; +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.get.MultiGetItemResponse; +import org.opensearch.action.get.MultiGetRequest; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.search.ClearScrollRequest; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchScrollRequest; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.IndexNotFoundException; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.MultiMatchQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.index.reindex.BulkByScrollResponse; +import org.opensearch.index.reindex.DeleteByQueryAction; +import org.opensearch.index.reindex.DeleteByQueryRequest; +import org.opensearch.index.reindex.UpdateByQueryAction; +import org.opensearch.index.reindex.UpdateByQueryRequest; +import org.opensearch.script.Script; +import org.opensearch.script.ScriptType; +import org.opensearch.search.Scroll; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.common.DefaultObjectMapper; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; +import org.opensearch.security.spi.resources.sharing.CreatedBy; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.security.spi.resources.sharing.ShareWith; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.transport.client.Client; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; + +/** + * This class handles the creation and management of the resource sharing index. + * It provides methods to create the index, index resource sharing entries along with updates and deletion, retrieve shared resources. + * + * @opensearch.experimental + */ +public class ResourceSharingIndexHandler { + + private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexHandler.class); + + private final Client client; + + private final String resourceSharingIndex; + + private final ThreadPool threadPool; + + public ResourceSharingIndexHandler(final String indexName, final Client client, final ThreadPool threadPool) { + this.resourceSharingIndex = indexName; + this.client = client; + this.threadPool = threadPool; + } + + public final static Map INDEX_SETTINGS = Map.of( + "index.number_of_shards", + 1, + "index.auto_expand_replicas", + "0-all", + "index.hidden", + "true" + ); + + /** + * Creates the resource sharing index if it doesn't already exist. + * This method initializes the index with predefined mappings and settings + * for storing resource sharing information. + * The index will be created with the following structure: + * - source_idx (keyword): The source index containing the original document + * - resource_id (keyword): The ID of the shared resource + * - created_by (object): Information about the user who created the sharing + * - user (keyword): Username of the creator + * - share_with (object): Access control configuration for shared resources + * - [scope] (object): Name of the scope + * - users (array): List of users with access + * - roles (array): List of roles with access + * - backend_roles (array): List of backend roles with access + * + * @throws RuntimeException if there are issues reading/writing index settings + * or communicating with the cluster + */ + + public void createResourceSharingIndexIfAbsent(Callable callable) { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + + CreateIndexRequest cir = new CreateIndexRequest(resourceSharingIndex).settings(INDEX_SETTINGS).waitForActiveShards(1); + ActionListener cirListener = ActionListener.wrap(response -> { + LOGGER.info("Resource sharing index {} created.", resourceSharingIndex); + if (callable != null) { + callable.call(); + } + }, (failResponse) -> { + /* Index already exists, ignore and continue */ + LOGGER.info("Index {} already exists.", resourceSharingIndex); + try { + if (callable != null) { + callable.call(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + this.client.admin().indices().create(cir, cirListener); + } + } + + /** + * Creates or updates a resource sharing record in the dedicated resource sharing index. + * This method handles the persistence of sharing metadata for resources, including + * the creator information and sharing permissions. + * + * @param resourceId The unique identifier of the resource being shared + * @param resourceIndex The source index where the original resource is stored + * @param createdBy Object containing information about the user creating/updating the sharing + * @param shareWith Object containing the sharing permissions' configuration. Can be null for initial creation. + * When provided, it should contain the access control settings for different groups: + * { + * "scope": { + * "users": ["user1", "user2"], + * "roles": ["role1", "role2"], + * "backend_roles": ["backend_role1"] + * } + * } + * @return ResourceSharing Returns resourceSharing object if the operation was successful, null otherwise + * @throws IOException if there are issues with index operations or JSON processing + */ + public ResourceSharing indexResourceSharing(String resourceId, String resourceIndex, CreatedBy createdBy, ShareWith shareWith) + throws IOException { + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + ResourceSharing entry = new ResourceSharing(resourceIndex, resourceId, createdBy, shareWith); + + IndexRequest ir = client.prepareIndex(resourceSharingIndex) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(entry.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .setOpType(DocWriteRequest.OpType.CREATE) // only create if an entry doesn't exist + .request(); + + ActionListener irListener = ActionListener.wrap( + idxResponse -> LOGGER.info( + "Successfully created {} entry for resource {} in index {}.", + resourceSharingIndex, + resourceId, + resourceIndex + ), + (failResponse) -> { + LOGGER.error(failResponse.getMessage()); + } + ); + client.index(ir, irListener); + return entry; + } catch (Exception e) { + LOGGER.error("Failed to create {} entry.", resourceSharingIndex, e); + throw new ResourceSharingException("Failed to create " + resourceSharingIndex + " entry.", e); + } + } + + /** + * Fetches all resource sharing records that match the specified system index. This method retrieves + * a get of resource IDs associated with the given system index from the resource sharing index. + * + *

The method executes the following steps: + *

    + *
  1. Creates a search request with term query matching the system index
  2. + *
  3. Applies source filtering to only fetch resource_id field
  4. + *
  5. Executes the search with a limit of 10000 documents
  6. + *
  7. Processes the results to extract resource IDs
  8. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "term": {
+     *       "source_idx": "resource_index_name"
+     *     }
+     *   },
+     *   "_source": ["resource_id"],
+     *   "size": 10000
+     * }
+     * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @apiNote This method: + *
    + *
  • Uses source filtering for optimal performance
  • + *
  • Performs exact matching on the source_idx field
  • + *
  • Returns an empty get instead of throwing exceptions
  • + *
+ */ + public void fetchAllDocuments(String pluginIndex, ActionListener> listener) { + LOGGER.debug("Fetching all documents asynchronously from {} where source_idx = {}", resourceSharingIndex, pluginIndex); + + try (final ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query( + QueryBuilders.termQuery("source_idx.keyword", pluginIndex) + ).size(10000).fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + client.search(searchRequest, new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + try { + Set resourceIds = new HashSet<>(); + + SearchHit[] hits = searchResponse.getHits().getHits(); + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + LOGGER.debug("Found {} documents in {} for source_idx: {}", resourceIds.size(), resourceSharingIndex, pluginIndex); + + listener.onResponse(resourceIds); + } catch (Exception e) { + LOGGER.error( + "Error while processing search response from {} for source_idx: {}", + resourceSharingIndex, + pluginIndex, + e + ); + listener.onFailure(e); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); + listener.onFailure(e); + } + }); + } catch (Exception e) { + LOGGER.error("Failed to initiate fetch documents from {} for source_idx: {}", resourceSharingIndex, pluginIndex, e); + listener.onFailure(e); + } + } + + /** + * Fetches documents that match the specified system index and have specific access type values. + * This method uses scroll API to handle large result sets efficiently. + * + *

The method executes the following steps: + *

    + *
  1. Validates the RecipientType parameter
  2. + *
  3. Creates a scrolling search request with a compound query
  4. + *
  5. Processes results in batches using scroll API
  6. + *
  7. Collects all matching resource IDs
  8. + *
  9. Cleans up scroll context
  10. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": "resource_index_name" } },
+     *         {
+     *           "bool": {
+     *             "should": [
+     *               {
+     *                 "nested": {
+     *                   "path": "share_with.*.RecipientType",
+     *                   "query": {
+     *                     "term": { "share_with.*.RecipientType": "entity_value" }
+     *                   }
+     *                 }
+     *               }
+     *             ],
+     *             "minimum_should_match": 1
+     *           }
+     *         }
+     *       ]
+     *     }
+     *   },
+     *   "_source": ["resource_id"],
+     *   "size": 1000
+     * }
+     * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param entities Set of values to match in the specified RecipientType field + * @param recipientType The type of association with the resource. Must be one of: + *
    + *
  • "users" - for user-based access
  • + *
  • "roles" - for role-based access
  • + *
  • "backend_roles" - for backend role-based access
  • + *
+ * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @throws RuntimeException if the search operation fails + * @apiNote This method: + *
    + *
  • Uses scroll API with 1-minute timeout
  • + *
  • Processes results in batches of 1000 documents
  • + *
  • Performs source filtering for optimization
  • + *
  • Uses nested queries for accessing array elements
  • + *
  • Properly cleans up scroll context after use
  • + *
+ */ + + public void fetchDocumentsForAllScopes( + String pluginIndex, + Set entities, + String recipientType, + ActionListener> listener + ) { + // "*" must match all scopes + fetchDocumentsForAGivenScope(pluginIndex, entities, recipientType, "*", listener); + } + + /** + * Fetches documents that match the specified system index and have specific access type values for a given scope. + * This method uses scroll API to handle large result sets efficiently. + * + *

The method executes the following steps: + *

    + *
  1. Validates the RecipientType parameter
  2. + *
  3. Creates a scrolling search request with a compound query
  4. + *
  5. Processes results in batches using scroll API
  6. + *
  7. Collects all matching resource IDs
  8. + *
  9. Cleans up scroll context
  10. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": "resource_index_name" } },
+     *         {
+     *           "bool": {
+     *             "should": [
+     *               {
+     *                 "nested": {
+     *                   "path": "share_with.scope.RecipientType",
+     *                   "query": {
+     *                     "term": { "share_with.scope.RecipientType": "entity_value" }
+     *                   }
+     *                 }
+     *               }
+     *             ],
+     *             "minimum_should_match": 1
+     *           }
+     *         }
+     *       ]
+     *     }
+     *   },
+     *   "_source": ["resource_id"],
+     *   "size": 1000
+     * }
+     * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param entities Set of values to match in the specified RecipientType field + * @param recipientType The type of association with the resource. Must be one of: + *
    + *
  • "users" - for user-based access
  • + *
  • "roles" - for role-based access
  • + *
  • "backend_roles" - for backend role-based access
  • + *
+ * @param scope The scope of the access. Should be implementation of {@link org.opensearch.security.spi.resources.ResourceAccessScope} + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @throws RuntimeException if the search operation fails + * @apiNote This method: + *
    + *
  • Uses scroll API with 1-minute timeout
  • + *
  • Processes results in batches of 1000 documents
  • + *
  • Performs source filtering for optimization
  • + *
  • Uses nested queries for accessing array elements
  • + *
  • Properly cleans up scroll context after use
  • + *
+ */ + public void fetchDocumentsForAGivenScope( + String pluginIndex, + Set entities, + String recipientType, + String scope, + ActionListener> listener + ) { + LOGGER.debug( + "Fetching documents asynchronously from index: {}, where share_with.{}.{} contains any of {}", + pluginIndex, + scope, + recipientType, + entities + ); + + final Set resourceIds = new HashSet<>(); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + searchRequest.scroll(scroll); + + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)); + + BoolQueryBuilder shouldQuery = QueryBuilders.boolQuery(); + if ("*".equals(scope)) { + for (String entity : entities) { + shouldQuery.should( + QueryBuilders.multiMatchQuery(entity, "share_with.*." + recipientType + ".keyword") + .type(MultiMatchQueryBuilder.Type.BEST_FIELDS) + ); + } + } else { + for (String entity : entities) { + shouldQuery.should(QueryBuilders.termQuery("share_with." + scope + "." + recipientType + ".keyword", entity)); + } + } + shouldQuery.minimumShouldMatch(1); + + boolQuery.must(QueryBuilders.existsQuery("share_with")).must(shouldQuery); + + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { + LOGGER.debug("Found {} documents matching the criteria in {}", resourceIds.size(), resourceSharingIndex); + listener.onResponse(resourceIds); + + }, exception -> { + LOGGER.error( + "Search failed for pluginIndex={}, scope={}, recipientType={}, entities={}", + pluginIndex, + scope, + recipientType, + entities, + exception + ); + listener.onFailure(exception); + + })); + } catch (Exception e) { + LOGGER.error( + "Failed to initiate fetch from {} for criteria - pluginIndex: {}, scope: {}, RecipientType: {}, entities: {}", + resourceSharingIndex, + pluginIndex, + scope, + recipientType, + entities, + e + ); + listener.onFailure(new RuntimeException("Failed to fetch documents: " + e.getMessage(), e)); + } + } + + /** + * Fetches documents from the resource sharing index that match a specific field value. + * This method uses scroll API to efficiently handle large result sets and performs exact + * matching on both system index and the specified field. + * + *

The method executes the following steps: + *

    + *
  1. Validates input parameters for null/empty values
  2. + *
  3. Creates a scrolling search request with a bool query
  4. + *
  5. Processes results in batches using scroll API
  6. + *
  7. Extracts resource IDs from matching documents
  8. + *
  9. Cleans up scroll context after completion
  10. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": "system_index_value" } },
+     *         { "term": { "field_name": "field_value" } }
+     *       ]
+     *     }
+     *   },
+     *   "_source": ["resource_id"],
+     *   "size": 1000
+     * }
+     * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param field The field name to search in. Must be a valid field in the index mapping + * @param value The value to match for the specified field. Performs exact term matching + * @param listener The listener to be notified when the operation completes. + * The listener receives a set of resource IDs as a result. + * @throws IllegalArgumentException if any parameter is null or empty + * @throws RuntimeException if the search operation fails, wrapping the underlying exception + * @apiNote This method: + *
    + *
  • Uses scroll API with 1-minute timeout for handling large result sets
  • + *
  • Performs exact term matching (not analyzed) on field values
  • + *
  • Processes results in batches of 1000 documents
  • + *
  • Uses source filtering to only fetch resource_id field
  • + *
  • Automatically cleans up scroll context after use
  • + *
+ *

+ * Example usage: + *

+     * Set resources = fetchDocumentsByField("myIndex", "status", "active");
+     * 
+ */ + public void fetchDocumentsByField(String pluginIndex, String field, String value, ActionListener> listener) { + if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(field) || StringUtils.isBlank(value)) { + listener.onFailure(new IllegalArgumentException("pluginIndex, field, and value must not be null or empty")); + return; + } + + LOGGER.debug("Fetching documents from index: {}, where {} = {}", pluginIndex, field, value); + + Set resourceIds = new HashSet<>(); + final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L)); + + // TODO: Once stashContext is replaced with switchContext this call will have to be modified + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex); + searchRequest.scroll(scroll); + + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) + .must(QueryBuilders.termQuery(field + ".keyword", value)); + + executeSearchRequest(resourceIds, scroll, searchRequest, boolQuery, ActionListener.wrap(success -> { + LOGGER.debug("Found {} documents in {} where {} = {}", resourceIds.size(), resourceSharingIndex, field, value); + listener.onResponse(resourceIds); + }, exception -> { + LOGGER.error("Failed to fetch documents from {} where {} = {}", resourceSharingIndex, field, value, exception); + listener.onFailure(new RuntimeException("Failed to fetch documents: " + exception.getMessage(), exception)); + })); + } catch (Exception e) { + LOGGER.error("Failed to initiate fetch from {} where {} = {}", resourceSharingIndex, field, value, e); + listener.onFailure(new RuntimeException("Failed to initiate fetch: " + e.getMessage(), e)); + } + + } + + /** + * Fetches a specific resource sharing document by its resource ID and system index. + * This method performs an exact match search and parses the result into a ResourceSharing object. + * + *

The method executes the following steps: + *

    + *
  1. Validates input parameters for null/empty values
  2. + *
  3. Creates a search request with a bool query for exact matching
  4. + *
  5. Executes the search with a limit of 1 document
  6. + *
  7. Parses the result using XContent parser if found
  8. + *
  9. Returns null if no matching document exists
  10. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": "resource_index_name" } },
+     *         { "term": { "resource_id": "resource_id_value" } }
+     *       ]
+     *     }
+     *   },
+     *   "size": 1
+     * }
+     * 
+ * + * @param pluginIndex The source index to match against the source_idx field + * @param resourceId The resource ID to fetch. Must exactly match the resource_id field + * @param listener The listener to be notified when the operation completes. + * The listener receives the parsed ResourceSharing object or null if not found + * @throws IllegalArgumentException if pluginIndexName or resourceId is null or empty + * @throws RuntimeException if the search operation fails or parsing errors occur, + * wrapping the underlying exception + * @apiNote This method: + *
    + *
  • Uses term queries for exact matching
  • + *
  • Expects only one matching document per resource ID
  • + *
  • Uses XContent parsing for consistent object creation
  • + *
  • Returns null instead of throwing exceptions for non-existent documents
  • + *
  • Provides detailed logging for troubleshooting
  • + *
+ *

+ * Example usage: + *

+     * ResourceSharing sharing = fetchDocumentById("myIndex", "resource123");
+     * if (sharing != null) {
+     *     // Process the resource sharing object
+     * }
+     * 
+ */ + public void fetchDocumentById(String pluginIndex, String resourceId, ActionListener listener) { + if (StringUtils.isBlank(pluginIndex) || StringUtils.isBlank(resourceId)) { + listener.onFailure(new IllegalArgumentException("pluginIndex and resourceId must not be null or empty")); + return; + } + LOGGER.debug("Fetching document from index: {}, resourceId: {}", pluginIndex, resourceId); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", pluginIndex)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); + + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery).size(1); // There is only one document for + // a single resource + + SearchRequest searchRequest = new SearchRequest(resourceSharingIndex).source(searchSourceBuilder); + + client.search(searchRequest, new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + try { + SearchHit[] hits = searchResponse.getHits().getHits(); + if (hits.length == 0) { + LOGGER.debug("No document found for resourceId: {} in index: {}", resourceId, pluginIndex); + listener.onResponse(null); + return; + } + + SearchHit hit = hits[0]; + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, hit.getSourceAsString()) + ) { + parser.nextToken(); + ResourceSharing resourceSharing = ResourceSharing.fromXContent(parser); + + LOGGER.debug("Successfully fetched document for resourceId: {} from index: {}", resourceId, pluginIndex); + + listener.onResponse(resourceSharing); + } + } catch (Exception e) { + LOGGER.error("Failed to parse document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new ResourceSharingException( + "Failed to parse document for resourceId: " + resourceId + " from index: " + pluginIndex, + e + ) + ); + } + } + + @Override + public void onFailure(Exception e) { + + LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new ResourceSharingException( + "Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, + e + ) + ); + + } + }); + } catch (Exception e) { + LOGGER.error("Failed to fetch document for resourceId: {} from index: {}", resourceId, pluginIndex, e); + listener.onFailure( + new ResourceSharingException("Failed to fetch document for resourceId: " + resourceId + " from index: " + pluginIndex, e) + ); + } + } + + /** + * Updates the sharing configuration for an existing resource in the resource sharing index. + * NOTE: This method only grants new access. To remove access use {@link #revokeAccess(String, String, Map, Set, String, boolean, ActionListener)} + * This method modifies the sharing permissions for a specific resource identified by its + * resource ID and source index. + * + * @param resourceId The unique identifier of the resource whose sharing configuration needs to be updated + * @param sourceIdx The source index where the original resource is stored + * @param requestUserName The user requesting to revoke the resource + * @param shareWith Updated sharing configuration object containing access control settings: + * { + * "scope": { + * "users": ["user1", "user2"], + * "roles": ["role1", "role2"], + * "backend_roles": ["backend_role1"] + * } + * } + * @param isAdmin Boolean indicating whether the user requesting to revoke is an admin or not + * @param listener Listener to be notified when the operation completes + * @throws RuntimeException if there's an error during the update operation + */ + public void updateResourceSharingInfo( + String resourceId, + String sourceIdx, + String requestUserName, + ShareWith shareWith, + boolean isAdmin, + ActionListener listener + ) { + XContentBuilder builder; + Map shareWithMap; + try { + builder = XContentFactory.jsonBuilder(); + shareWith.toXContent(builder, ToXContent.EMPTY_PARAMS); + String json = builder.toString(); + shareWithMap = DefaultObjectMapper.readValue(json, new TypeReference<>() { + }); + } catch (IOException e) { + LOGGER.error("Failed to build json content", e); + listener.onFailure(new ResourceSharingException("Failed to build json content", e)); + return; + } + + StepListener fetchDocListener = new StepListener<>(); + StepListener updateScriptListener = new StepListener<>(); + StepListener updatedSharingListener = new StepListener<>(); + + // Fetch resource sharing doc + fetchDocumentById(sourceIdx, resourceId, fetchDocListener); + + // build update script + fetchDocListener.whenComplete(currentSharingInfo -> { + // Check if user can share. At present only the resource creator and admin is allowed to share the resource + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { + + LOGGER.error("User {} is not authorized to share resource {}", requestUserName, resourceId); + listener.onFailure( + new ResourceSharingException("User " + requestUserName + " is not authorized to share resource " + resourceId) + ); + } + + Script updateScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with == null) { + ctx._source.share_with = [:]; + } + + for (def entry : params.shareWith.entrySet()) { + def scopeName = entry.getKey(); + def newScope = entry.getValue(); + + if (!ctx._source.share_with.containsKey(scopeName)) { + def newScopeEntry = [:]; + for (def field : newScope.entrySet()) { + if (field.getValue() != null && !field.getValue().isEmpty()) { + newScopeEntry[field.getKey()] = new HashSet(field.getValue()); + } + } + ctx._source.share_with[scopeName] = newScopeEntry; + } else { + def existingScope = ctx._source.share_with[scopeName]; + + for (def field : newScope.entrySet()) { + def fieldName = field.getKey(); + def newValues = field.getValue(); + + if (newValues != null && !newValues.isEmpty()) { + if (!existingScope.containsKey(fieldName)) { + existingScope[fieldName] = new HashSet(); + } + + for (def value : newValues) { + if (!existingScope[fieldName].contains(value)) { + existingScope[fieldName].add(value); + } + } + } + } + } + } + """, Collections.singletonMap("shareWith", shareWithMap)); + + updateByQueryResourceSharing(sourceIdx, resourceId, updateScript, updateScriptListener); + + }, listener::onFailure); + + // Build & return the updated ResourceSharing + updateScriptListener.whenComplete(success -> { + if (!success) { + LOGGER.error("Failed to update resource sharing info for resource {}", resourceId); + listener.onResponse(null); + return; + } + // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory + // intensive to do it in java) + fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); + }, listener::onFailure); + + updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); + } + + /** + * Revokes access for specified entities from a resource sharing document. This method removes the specified + * entities (users, roles, or backend roles) from the existing sharing configuration while preserving other + * sharing settings. + * + *

The method performs the following steps: + *

    + *
  1. Fetches the existing document
  2. + *
  3. Removes specified entities from their respective lists in all sharing groups
  4. + *
  5. Updates the document if modifications were made
  6. + *
  7. Returns the updated resource sharing configuration
  8. + *
+ * + *

Example document structure: + *

+     * {
+     *   "source_idx": "resource_index_name",
+     *   "resource_id": "resource_id",
+     *   "share_with": {
+     *     "scope": {
+     *       "users": ["user1", "user2"],
+     *       "roles": ["role1", "role2"],
+     *       "backend_roles": ["backend_role1"]
+     *     }
+     *   }
+     * }
+     * 
+ * + * @param resourceId The ID of the resource from which to revoke access + * @param sourceIdx The name of the system index where the resource exists + * @param revokeAccess A map containing entity types (USER, ROLE, BACKEND_ROLE) and their corresponding + * values to be removed from the sharing configuration + * @param scopes A get of scopes to revoke access from. If null or empty, access is revoked from all scopes + * @param requestUserName The user trying to revoke the accesses + * @param isAdmin Boolean indicating whether the user is an admin or not + * @param listener Listener to be notified when the operation completes + * @throws IllegalArgumentException if resourceId, sourceIdx is null/empty, or if revokeAccess is null/empty + * @throws RuntimeException if the update operation fails or encounters an error + * @apiNote This method modifies the existing document. If no modifications are needed (i.e., specified + * entities don't exist in the current configuration), the original document is returned unchanged. + * @example + *
+     * Map> revokeAccess = new HashMap<>();
+     * revokeAccess.put(RecipientType.USER, Set.of("user1", "user2"));
+     * revokeAccess.put(RecipientType.ROLE, Set.of("role1"));
+     * ResourceSharing updated = revokeAccess("resourceId", "pluginIndex", revokeAccess);
+     * 
+ * @see RecipientType + * @see ResourceSharing + */ + public void revokeAccess( + String resourceId, + String sourceIdx, + Map> revokeAccess, + Set scopes, + String requestUserName, + boolean isAdmin, + ActionListener listener + ) { + if (StringUtils.isBlank(resourceId) || StringUtils.isBlank(sourceIdx) || revokeAccess == null || revokeAccess.isEmpty()) { + listener.onFailure(new IllegalArgumentException("resourceId, sourceIdx, and revokeAccess must not be null or empty")); + return; + } + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + + LOGGER.debug( + "Revoking access for resource {} in {} for entities: {} and scopes: {}", + resourceId, + sourceIdx, + revokeAccess, + scopes + ); + + StepListener currentSharingListener = new StepListener<>(); + StepListener revokeUpdateListener = new StepListener<>(); + StepListener updatedSharingListener = new StepListener<>(); + + // Fetch the current ResourceSharing document + fetchDocumentById(sourceIdx, resourceId, currentSharingListener); + + // Check permissions & build revoke script + currentSharingListener.whenComplete(currentSharingInfo -> { + // Only admin or the creator of the resource is currently allowed to revoke access + if (!isAdmin && currentSharingInfo != null && !currentSharingInfo.getCreatedBy().getCreator().equals(requestUserName)) { + listener.onFailure( + new ResourceSharingException( + "User " + requestUserName + " is not authorized to revoke access to resource " + resourceId + ) + ); + } + + Map revoke = new HashMap<>(); + for (Map.Entry> entry : revokeAccess.entrySet()) { + revoke.put(entry.getKey().type().toLowerCase(), new ArrayList<>(entry.getValue())); + } + List scopesToUse = (scopes != null) ? new ArrayList<>(scopes) : new ArrayList<>(); + + Script revokeScript = new Script(ScriptType.INLINE, "painless", """ + if (ctx._source.share_with != null) { + Set scopesToProcess = new HashSet(params.scopes.isEmpty() ? ctx._source.share_with.keySet() : params.scopes); + + for (def scopeName : scopesToProcess) { + if (ctx._source.share_with.containsKey(scopeName)) { + def existingScope = ctx._source.share_with.get(scopeName); + + for (def entry : params.revokeAccess.entrySet()) { + def RecipientType = entry.getKey(); + def entitiesToRemove = entry.getValue(); + + if (existingScope.containsKey(RecipientType) && existingScope[RecipientType] != null) { + if (!(existingScope[RecipientType] instanceof HashSet)) { + existingScope[RecipientType] = new HashSet(existingScope[RecipientType]); + } + + existingScope[RecipientType].removeAll(entitiesToRemove); + + if (existingScope[RecipientType].isEmpty()) { + existingScope.remove(RecipientType); + } + } + } + + if (existingScope.isEmpty()) { + ctx._source.share_with.remove(scopeName); + } + } + } + } + """, Map.of("revokeAccess", revoke, "scopes", scopesToUse)); + updateByQueryResourceSharing(sourceIdx, resourceId, revokeScript, revokeUpdateListener); + + }, listener::onFailure); + + // Return doc or null based on successful result, fail otherwise + revokeUpdateListener.whenComplete(success -> { + if (!success) { + LOGGER.error("Failed to revoke access for resource {} in index {} (no docs updated).", resourceId, sourceIdx); + listener.onResponse(null); + return; + } + // TODO check if this should be replaced by Java in-memory computation (current intuition is that it will be more memory + // intensive to do it in java) + fetchDocumentById(sourceIdx, resourceId, updatedSharingListener); + }, listener::onFailure); + + updatedSharingListener.whenComplete(listener::onResponse, listener::onFailure); + } + } + + /** + * Deletes resource sharing records that match the specified source index and resource ID. + * This method performs a delete-by-query operation in the resource sharing index. + * + *

The method executes the following steps: + *

    + *
  1. Creates a delete-by-query request with a bool query
  2. + *
  3. Matches documents based on exact source index and resource ID
  4. + *
  5. Executes the delete operation with immediate refresh
  6. + *
  7. Returns the success/failure status based on deletion results
  8. + *
+ * + *

Example document structure that will be deleted: + *

+     * {
+     *   "source_idx": "source_index_name",
+     *   "resource_id": "resource_id_value",
+     *   "share_with": {
+     *     // sharing configuration
+     *   }
+     * }
+     * 
+ * + * @param sourceIdx The source index to match in the query (exact match) + * @param resourceId The resource ID to match in the query (exact match) + * @param listener The listener to be notified when the operation completes + * @throws IllegalArgumentException if sourceIdx or resourceId is null/empty + * @throws RuntimeException if the delete operation fails or encounters an error + * @implNote The delete operation uses a bool query with two must clauses to ensure exact matching: + *
+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx": sourceIdx } },
+     *         { "term": { "resource_id": resourceId } }
+     *       ]
+     *     }
+     *   }
+     * }
+     * 
+ */ + public void deleteResourceSharingRecord(String resourceId, String sourceIdx, ActionListener listener) { + LOGGER.debug( + "Deleting documents asynchronously from {} where source_idx = {} and resource_id = {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + DeleteByQueryRequest dbq = new DeleteByQueryRequest(resourceSharingIndex).setQuery( + QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)) + ).setRefresh(true); + + client.execute(DeleteByQueryAction.INSTANCE, dbq, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + + long deleted = response.getDeleted(); + if (deleted > 0) { + LOGGER.debug("Successfully deleted {} documents from {}", deleted, resourceSharingIndex); + listener.onResponse(true); + } else { + LOGGER.debug( + "No documents found to delete in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + // No documents were deleted + listener.onResponse(false); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to delete documents from {}", resourceSharingIndex, e); + listener.onFailure(e); + + } + }); + } catch (Exception e) { + LOGGER.error("Failed to delete documents from {} before request submission", resourceSharingIndex, e); + listener.onFailure(e); + } + } + + /** + * Deletes all resource sharing records that were created by a specific user. + * This method performs a delete-by-query operation to remove all documents where + * the created_by.user field matches the specified username. + * + *

The method executes the following steps: + *

    + *
  1. Validates the input username parameter
  2. + *
  3. Creates a delete-by-query request with term query matching
  4. + *
  5. Executes the delete operation with immediate refresh
  6. + *
  7. Returns the operation status based on number of deleted documents
  8. + *
+ * + *

Example query structure: + *

+     * {
+     *   "query": {
+     *     "term": {
+     *       "created_by.user": "username"
+     *     }
+     *   }
+     * }
+     * 
+ * + * @param name The username to match against the created_by.user field + * @param listener The listener to be notified when the operation completes + * @throws IllegalArgumentException if name is null or empty + * @implNote Implementation details: + *
    + *
  • Uses DeleteByQueryRequest for efficient bulk deletion
  • + *
  • Sets refresh=true for immediate consistency
  • + *
  • Uses term query for exact username matching
  • + *
  • Implements comprehensive error handling and logging
  • + *
+ *

+ * Example usage: + *

+     * boolean success = deleteAllRecordsForUser("john.doe");
+     * if (success) {
+     *     // Records were successfully deleted
+     * } else {
+     *     // No matching records found or operation failed
+     * }
+     * 
+ */ + public void deleteAllRecordsForUser(String name, ActionListener listener) { + if (StringUtils.isBlank(name)) { + listener.onFailure(new IllegalArgumentException("Username must not be null or empty")); + return; + } + + LOGGER.debug("Deleting all records for user {} asynchronously", name); + + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + DeleteByQueryRequest deleteRequest = new DeleteByQueryRequest(resourceSharingIndex).setQuery( + QueryBuilders.termQuery("created_by.user", name) + ).setRefresh(true); + + client.execute(DeleteByQueryAction.INSTANCE, deleteRequest, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + long deletedDocs = response.getDeleted(); + if (deletedDocs > 0) { + LOGGER.debug("Successfully deleted {} documents created by user {}", deletedDocs, name); + listener.onResponse(true); + } else { + LOGGER.warn("No documents found for user {}", name); + // No documents matched => success = false + listener.onResponse(false); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to delete documents for user {}", name, e); + listener.onFailure(e); + } + }); + } catch (Exception e) { + LOGGER.error("Failed to delete documents for user {} before request submission", name, e); + listener.onFailure(e); + } + } + + /** + * Fetches all documents from the specified resource index and deserializes them into the specified class. + * + * @param resourceIndex The resource index to fetch documents from. + * @param parser The class to deserialize the documents into a specified type defined by the parser. + * @param listener The listener to be notified with the set of deserialized documents. + * @param The type of the deserialized documents. + */ + public void getResourceDocumentsFromIds( + Set resourceIds, + String resourceIndex, + ResourceParser parser, + ActionListener> listener + ) { + if (resourceIds.isEmpty()) { + listener.onResponse(new HashSet<>()); + return; + } + + // stashing Context to avoid permission issues in-case resourceIndex is a system index + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + MultiGetRequest request = new MultiGetRequest(); + for (String id : resourceIds) { + request.add(new MultiGetRequest.Item(resourceIndex, id)); + } + + client.multiGet(request, ActionListener.wrap(response -> { + Set result = new HashSet<>(); + try { + for (MultiGetItemResponse itemResponse : response.getResponses()) { + if (!itemResponse.isFailed() && itemResponse.getResponse().isExists()) { + BytesReference sourceAsString = itemResponse.getResponse().getSourceAsBytesRef(); + XContentParser xContentParser = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + sourceAsString, + XContentType.JSON + ); + T resource = parser.parseXContent(xContentParser); + result.add(resource); + } + } + listener.onResponse(result); + } catch (Exception e) { + listener.onFailure(new ResourceSharingException("Failed to parse resources: " + e.getMessage(), e)); + } + }, e -> { + if (e instanceof IndexNotFoundException) { + LOGGER.error("Index {} does not exist", resourceIndex, e); + listener.onFailure(e); + } else { + LOGGER.error("Failed to fetch resources with ids {} from index {}", resourceIds, resourceIndex, e); + listener.onFailure(new ResourceSharingException("Failed to fetch resources: " + e.getMessage(), e)); + } + })); + } + } + + /** + * Updates resource sharing entries that match the specified source index and resource ID + * using the provided update script. This method performs an update-by-query operation + * in the resource sharing index. + * + *

The method executes the following steps: + *

    + *
  1. Creates a bool query to match exact source index and resource ID
  2. + *
  3. Constructs an update-by-query request with the query and update script
  4. + *
  5. Executes the update operation
  6. + *
  7. Returns success/failure status based on update results
  8. + *
+ * + *

Example document matching structure: + *

+     * {
+     *   "source_idx": "source_index_name",
+     *   "resource_id": "resource_id_value",
+     *   "share_with": {
+     *     // sharing configuration to be updated
+     *   }
+     * }
+     * 
+ * + * @param sourceIdx The source index to match in the query (exact match) + * @param resourceId The resource ID to match in the query (exact match) + * @param updateScript The script containing the update operations to be performed. + * This script defines how the matching documents should be modified + * @param listener Listener to be notified when the operation completes + * @apiNote This method: + *
    + *
  • Uses term queries for exact matching of source_idx and resource_id
  • + *
  • Returns false for both "no matching documents" and "operation failure" cases
  • + *
  • Logs the complete update request for debugging purposes
  • + *
  • Provides detailed logging for success and failure scenarios
  • + *
+ * @implNote The update operation uses a bool query with two must clauses: + *
+     * {
+     *   "query": {
+     *     "bool": {
+     *       "must": [
+     *         { "term": { "source_idx.keyword": sourceIdx } },
+     *         { "term": { "resource_id.keyword": resourceId } }
+     *       ]
+     *     }
+     *   }
+     * }
+     * 
+ */ + private void updateByQueryResourceSharing(String sourceIdx, String resourceId, Script updateScript, ActionListener listener) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + BoolQueryBuilder query = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("source_idx.keyword", sourceIdx)) + .must(QueryBuilders.termQuery("resource_id.keyword", resourceId)); + + UpdateByQueryRequest ubq = new UpdateByQueryRequest(resourceSharingIndex).setQuery(query) + .setScript(updateScript) + .setRefresh(true); + + client.execute(UpdateByQueryAction.INSTANCE, ubq, new ActionListener<>() { + @Override + public void onResponse(BulkByScrollResponse response) { + long updated = response.getUpdated(); + if (updated > 0) { + LOGGER.debug("Successfully updated {} documents in {}.", updated, resourceSharingIndex); + listener.onResponse(true); + } else { + LOGGER.debug( + "No documents found to update in {} for source_idx: {} and resource_id: {}", + resourceSharingIndex, + sourceIdx, + resourceId + ); + listener.onResponse(false); + } + } + + @Override + public void onFailure(Exception e) { + LOGGER.error("Failed to update documents in {}.", resourceSharingIndex, e); + listener.onFailure(e); + + } + }); + } catch (Exception e) { + LOGGER.error("Failed to update documents in {} before request submission.", resourceSharingIndex, e); + listener.onFailure(e); + } + } + + /** + * Helper method to execute a search request and collect resource IDs from the results. + * + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param searchRequest Request to execute + * @param boolQuery Query to execute with the request + * @param listener Listener to be notified when the operation completes + */ + private void executeSearchRequest( + Set resourceIds, + Scroll scroll, + SearchRequest searchRequest, + BoolQueryBuilder boolQuery, + ActionListener listener + ) { + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().query(boolQuery) + .size(1000) + .fetchSource(new String[] { "resource_id" }, null); + + searchRequest.source(searchSourceBuilder); + + StepListener searchStep = new StepListener<>(); + + client.search(searchRequest, searchStep); + + searchStep.whenComplete(initialResponse -> { + String scrollId = initialResponse.getScrollId(); + processScrollResults(resourceIds, scroll, scrollId, initialResponse.getHits().getHits(), listener); + }, listener::onFailure); + } + + /** + * Helper method to process scroll results recursively. + * + * @param resourceIds List to collect resource IDs + * @param scroll Search Scroll + * @param scrollId Scroll ID + * @param hits Search hits + * @param listener Listener to be notified when the operation completes + */ + private void processScrollResults( + Set resourceIds, + Scroll scroll, + String scrollId, + SearchHit[] hits, + ActionListener listener + ) { + // If no hits, clean up and complete + if (hits == null || hits.length == 0) { + clearScroll(scrollId, listener); + return; + } + + // Process current batch of hits + for (SearchHit hit : hits) { + Map sourceAsMap = hit.getSourceAsMap(); + if (sourceAsMap != null && sourceAsMap.containsKey("resource_id")) { + resourceIds.add(sourceAsMap.get("resource_id").toString()); + } + } + + // Prepare next scroll request + SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId); + scrollRequest.scroll(scroll); + + // Execute next scroll + client.searchScroll(scrollRequest, ActionListener.wrap(scrollResponse -> { + // Process next batch recursively + processScrollResults(resourceIds, scroll, scrollResponse.getScrollId(), scrollResponse.getHits().getHits(), listener); + }, e -> { + // Clean up scroll context on failure + clearScroll(scrollId, ActionListener.wrap(r -> listener.onFailure(e), ex -> { + e.addSuppressed(ex); + listener.onFailure(e); + })); + })); + } + + /** + * Helper method to clear scroll context. + * + * @param scrollId Scroll ID + * @param listener Listener to be notified when the operation completes + */ + private void clearScroll(String scrollId, ActionListener listener) { + if (scrollId == null) { + listener.onResponse(null); + return; + } + + ClearScrollRequest clearScrollRequest = new ClearScrollRequest(); + clearScrollRequest.addScrollId(scrollId); + + client.clearScroll(clearScrollRequest, ActionListener.wrap(r -> listener.onResponse(null), e -> { + LOGGER.warn("Failed to clear scroll context", e); + listener.onResponse(null); + })); + } + +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java new file mode 100644 index 0000000000..166d410f86 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java @@ -0,0 +1,59 @@ +/* + * 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.common.resources; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +/** + * This class is responsible for managing the resource sharing index. + * It provides methods to create the index if it doesn't exist. + * + * @opensearch.experimental + */ +public class ResourceSharingIndexManagementRepository { + + private static final Logger LOGGER = LogManager.getLogger(ResourceSharingIndexManagementRepository.class); + + private final ResourceSharingIndexHandler resourceSharingIndexHandler; + private final boolean resourceSharingEnabled; + + protected ResourceSharingIndexManagementRepository( + final ResourceSharingIndexHandler resourceSharingIndexHandler, + boolean isResourceSharingEnabled + ) { + this.resourceSharingIndexHandler = resourceSharingIndexHandler; + this.resourceSharingEnabled = isResourceSharingEnabled; + } + + public static ResourceSharingIndexManagementRepository create( + ResourceSharingIndexHandler resourceSharingIndexHandler, + boolean isResourceSharingEnabled + ) { + return new ResourceSharingIndexManagementRepository(resourceSharingIndexHandler, isResourceSharingEnabled); + } + + /** + * Creates the resource sharing index if it doesn't already exist. + * This method is called during the initialization phase of the repository. + * It ensures that the index is set up with the necessary mappings and settings + * before any operations are performed on the index. + */ + public void createResourceSharingIndexIfAbsent() { + // TODO check if this should be wrapped in an atomic completable future + if (resourceSharingEnabled) { + LOGGER.debug("Attempting to create Resource Sharing index"); + this.resourceSharingIndexHandler.createResourceSharingIndexIfAbsent(() -> null); + } + + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java new file mode 100644 index 0000000000..5820d21a8c --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java @@ -0,0 +1,28 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.common.resources.rest; + +import org.opensearch.action.ActionType; + +/** + * This class represents the action type for resource access. + * It is used to execute the resource access request and retrieve the response. + * + * @opensearch.experimental + */ +public class ResourceAccessAction extends ActionType { + + public static final ResourceAccessAction INSTANCE = new ResourceAccessAction(); + + public static final String NAME = "cluster:admin/security/resource_access"; + + private ResourceAccessAction() { + super(NAME, ResourceAccessResponse::new); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java new file mode 100644 index 0000000000..1df9c244bb --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java @@ -0,0 +1,236 @@ +/* + * 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.common.resources.rest; + +import java.io.IOException; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.apache.commons.lang3.StringUtils; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.spi.resources.sharing.ShareWith; + +/** + * This class represents a request to access a resource. + * It encapsulates the operation, resource ID, resource index, scope, share with information, revoked entities, and scopes. + * + * @opensearch.experimental + */ +public class ResourceAccessRequest extends ActionRequest { + + public enum Operation { + LIST, + SHARE, + REVOKE, + VERIFY + } + + private final Operation operation; + private final String resourceId; + private final String resourceIndex; + private final ShareWith shareWith; + private final Map> revokedEntities; + private final Set scopes; + + /** + * Private constructor to enforce usage of Builder + */ + private ResourceAccessRequest(Builder builder) { + this.operation = builder.operation; + this.resourceId = builder.resourceId; + this.resourceIndex = builder.resourceIndex; + this.shareWith = builder.shareWith; + this.revokedEntities = builder.revokedEntities; + this.scopes = builder.scopes; + } + + /** + * Static factory method to initialize ResourceAccessRequest from a Map. + */ + @SuppressWarnings("unchecked") + public static ResourceAccessRequest from(Map source, Map params) throws IOException { + Builder builder = new Builder(); + + if (source.containsKey("operation")) { + builder.operation((Operation) source.get("operation")); + } else { + throw new IllegalArgumentException("Missing required field: operation"); + } + + builder.resourceId((String) source.get("resource_id")); + String resourceIndex = params.getOrDefault("resource_index", (String) source.get("resource_index")); + if (StringUtils.isEmpty(resourceIndex)) { + throw new IllegalArgumentException("Missing required field: resource_index"); + } + builder.resourceIndex(resourceIndex); + + if (source.containsKey("share_with")) { + builder.shareWith((Map) source.get("share_with")); + } + + if (source.containsKey("entities_to_revoke")) { + builder.revokedEntities((Map) source.get("entities_to_revoke")); + } + + if (source.containsKey("scopes")) { + builder.scopes(Set.copyOf((List) source.get("scopes"))); // Ensuring Set type + } + + return builder.build(); + } + + public ResourceAccessRequest(StreamInput in) throws IOException { + super(in); + this.operation = in.readEnum(Operation.class); + this.resourceId = in.readOptionalString(); + this.resourceIndex = in.readOptionalString(); + this.shareWith = in.readOptionalWriteable(ShareWith::new); + this.revokedEntities = in.readMap(StreamInput::readString, valIn -> valIn.readSet(StreamInput::readString)); + this.scopes = in.readSet(StreamInput::readString); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(operation); + out.writeOptionalString(resourceId); + out.writeOptionalString(resourceIndex); + out.writeOptionalWriteable(shareWith); + out.writeMap(revokedEntities, StreamOutput::writeString, StreamOutput::writeStringCollection); + out.writeStringCollection(scopes); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public Operation getOperation() { + return operation; + } + + public String getResourceId() { + return resourceId; + } + + public String getResourceIndex() { + return resourceIndex; + } + + public ShareWith getShareWith() { + return shareWith; + } + + public Map> getRevokedEntities() { + return revokedEntities; + } + + public Set getScopes() { + return scopes; + } + + /** + * Builder for ResourceAccessRequest + */ + public static class Builder { + private Operation operation; + private String resourceId; + private String resourceIndex; + private ShareWith shareWith; + private Map> revokedEntities; + private Set scopes; + + public Builder operation(Operation operation) { + this.operation = operation; + return this; + } + + public Builder resourceId(String resourceId) { + this.resourceId = resourceId; + return this; + } + + public Builder resourceIndex(String resourceIndex) { + this.resourceIndex = resourceIndex; + return this; + } + + public Builder shareWith(Map source) { + try { + this.shareWith = parseShareWith(source); + } catch (Exception e) { + this.shareWith = null; + } + return this; + } + + public Builder revokedEntities(Map source) { + try { + this.revokedEntities = parseRevokedEntities(source); + } catch (Exception e) { + this.revokedEntities = null; + } + return this; + } + + public Builder scopes(Set scopes) { + this.scopes = scopes; + return this; + } + + public ResourceAccessRequest build() { + return new ResourceAccessRequest(this); + } + + private ShareWith parseShareWith(Map source) throws IOException { + if (source == null || source.isEmpty()) { + throw new IllegalArgumentException("share_with is required and cannot be empty"); + } + + String jsonString = XContentFactory.jsonBuilder().map(source).toString(); + + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, jsonString) + ) { + + return ShareWith.fromXContent(parser); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid share_with structure: " + e.getMessage(), e); + } + } + + private Map> parseRevokedEntities(Map source) { + + return source.entrySet() + .stream() + .filter(entry -> entry.getValue() instanceof Collection) + .collect( + Collectors.toMap( + Map.Entry::getKey, + entry -> ((Collection) entry.getValue()).stream() + .filter(String.class::isInstance) + .map(String.class::cast) + .collect(Collectors.toSet()) + ) + ); + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java new file mode 100644 index 0000000000..880cfe00ec --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java @@ -0,0 +1,32 @@ +/* + * 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.common.resources.rest; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * This class is used to represent the request parameters for resource access. + * It implements the NamedWriteable interface to allow serialization and deserialization of the request parameters. + * + * @opensearch.experimental + */ +public class ResourceAccessRequestParams implements NamedWriteable { + @Override + public String getWriteableName() { + return "resource_access_request_params"; + } + + @Override + public void writeTo(StreamOutput streamOutput) throws IOException { + + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java new file mode 100644 index 0000000000..ac3ebf602f --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java @@ -0,0 +1,98 @@ +/* + * 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.common.resources.rest; + +import java.io.IOException; +import java.util.Collections; +import java.util.Set; + +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.sharing.ResourceSharing; + +/** + * This class is used to represent the response of a resource access request. + * It contains the response type and the response data. + * + * @opensearch.experimental + */ +public class ResourceAccessResponse extends ActionResponse implements ToXContentObject { + public enum ResponseType { + RESOURCES, + RESOURCE_SHARING, + BOOLEAN + } + + private final ResponseType responseType; + private final Object responseData; + + public ResourceAccessResponse(final StreamInput in) throws IOException { + this.responseType = in.readEnum(ResponseType.class); + this.responseData = null; + } + + public ResourceAccessResponse(Set resources) { + this.responseType = ResponseType.RESOURCES; + this.responseData = resources; + } + + public ResourceAccessResponse(ResourceSharing resourceSharing) { + this.responseType = ResponseType.RESOURCE_SHARING; + this.responseData = resourceSharing; + } + + public ResourceAccessResponse(boolean hasPermission) { + this.responseType = ResponseType.BOOLEAN; + this.responseData = hasPermission; + } + + @SuppressWarnings("unchecked") + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeEnum(responseType); + switch (responseType) { + case RESOURCES -> out.writeCollection((Set) responseData); + case RESOURCE_SHARING -> ((ResourceSharing) responseData).writeTo(out); + case BOOLEAN -> out.writeBoolean((Boolean) responseData); + } + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + switch (responseType) { + case RESOURCES -> builder.field("resources", responseData); + case RESOURCE_SHARING -> builder.field("sharing_info", responseData); + case BOOLEAN -> builder.field("has_permission", responseData); + } + return builder.endObject(); + } + + @SuppressWarnings("unchecked") + public Set getResources() { + return responseType == ResponseType.RESOURCES ? (Set) responseData : Collections.emptySet(); + } + + public ResourceSharing getResourceSharing() { + return responseType == ResponseType.RESOURCE_SHARING ? (ResourceSharing) responseData : null; + } + + public Boolean getHasPermission() { + return responseType == ResponseType.BOOLEAN ? (Boolean) responseData : null; + } + + @Override + public String toString() { + return "ResourceAccessResponse [responseType=" + responseType + ", responseData=" + responseData + "]"; + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java new file mode 100644 index 0000000000..700a064ed5 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java @@ -0,0 +1,151 @@ +/* + * 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.common.resources.rest; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.rest.RestStatus; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.BytesRestResponse; +import org.opensearch.rest.RestChannel; +import org.opensearch.rest.RestRequest; +import org.opensearch.transport.client.node.NodeClient; + +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.rest.RestRequest.Method.POST; +import static org.opensearch.security.common.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.common.dlic.rest.api.Responses.forbidden; +import static org.opensearch.security.common.dlic.rest.api.Responses.ok; +import static org.opensearch.security.common.dlic.rest.api.Responses.unauthorized; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.LIST; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.REVOKE; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.SHARE; +import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.VERIFY; +import static org.opensearch.security.common.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; +import static org.opensearch.security.common.support.Utils.addRoutesPrefix; + +/** + * This class handles the REST API for resource access management. + * It provides endpoints for listing, revoking, sharing, and verifying resource access. + * + * @opensearch.experimental + */ +public class ResourceAccessRestAction extends BaseRestHandler { + private static final Logger LOGGER = LogManager.getLogger(ResourceAccessRestAction.class); + + public ResourceAccessRestAction() {} + + @Override + public List routes() { + return addRoutesPrefix( + ImmutableList.of( + new Route(GET, "/list/{resource_index}"), + new Route(POST, "/revoke"), + new Route(POST, "/share"), + new Route(POST, "/verify_access") + ), + PLUGIN_RESOURCE_ROUTE_PREFIX + ); + } + + @Override + public String getName() { + return "resource_api_action"; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + consumeParams(request); // early consume params to avoid 400s + + Map source = new HashMap<>(); + if (request.hasContent()) { + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + } + + String path = request.path().split(PLUGIN_RESOURCE_ROUTE_PREFIX)[1].split("/")[1]; + switch (path) { + case "list" -> source.put("operation", LIST); + case "revoke" -> source.put("operation", REVOKE); + case "share" -> source.put("operation", SHARE); + case "verify_access" -> source.put("operation", VERIFY); + default -> { + return channel -> badRequest(channel, "Unknown route: " + path); + } + } + + ResourceAccessRequest resourceAccessRequest = ResourceAccessRequest.from(source, request.params()); + return channel -> { + client.executeLocally(ResourceAccessAction.INSTANCE, resourceAccessRequest, new ActionListener<>() { + + @Override + public void onResponse(ResourceAccessResponse response) { + try { + sendResponse(channel, response); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onFailure(Exception e) { + handleError(channel, e); + } + + }); + }; + } + + /** + * Consume params early to avoid 400s. + * + * @param request from which the params must be consumed + */ + private void consumeParams(RestRequest request) { + request.param("resource_index", ""); + } + + /** + * Send the appropriate response to the channel. + * @param channel the channel to send the response to + * @param response the response to send + * @throws IOException if an I/O error occurs + */ + private void sendResponse(RestChannel channel, ResourceAccessResponse response) throws IOException { + ok(channel, response::toXContent); + } + + /** + * Handle errors that occur during request processing. + * @param channel the channel to send the error response to + * @param e the exception that caused the error + */ + private void handleError(RestChannel channel, Exception e) { + String message = e.getMessage(); + LOGGER.error(message, e); + if (message.contains("not authorized")) { + forbidden(channel, message); + } else if (message.contains("no authenticated")) { + unauthorized(channel); + } else if (message.contains("not a system index")) { + badRequest(channel, message); + } + channel.sendResponse(new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, message)); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java new file mode 100644 index 0000000000..6bd58246c8 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java @@ -0,0 +1,117 @@ +/* + * 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.common.resources.rest; + +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.indices.SystemIndices; +import org.opensearch.security.common.resources.ResourceAccessHandler; +import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; +import org.opensearch.security.spi.resources.sharing.RecipientType; +import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +/** + * Transport action for handling resource access requests. + * + * @opensearch.experimental + */ +public class ResourceAccessTransportAction extends HandledTransportAction { + private final ResourceAccessHandler resourceAccessHandler; + + private final SystemIndices systemIndices; + + @Inject + public ResourceAccessTransportAction( + TransportService transportService, + ActionFilters actionFilters, + SystemIndices systemIndices, + ResourceAccessHandler resourceAccessHandler + ) { + super(ResourceAccessAction.NAME, transportService, actionFilters, ResourceAccessRequest::new); + this.systemIndices = systemIndices; + this.resourceAccessHandler = resourceAccessHandler; + } + + @Override + protected void doExecute(Task task, ResourceAccessRequest request, ActionListener actionListener) { + // verify that the request if for a system index + if (!this.systemIndices.isSystemIndex(request.getResourceIndex())) { + actionListener.onFailure( + new ResourceSharingException("Resource index '" + request.getResourceIndex() + "' is not a system index.") + ); + return; + } + + switch (request.getOperation()) { + case LIST: + handleListResources(request, actionListener); + break; + case SHARE: + handleGrantAccess(request, actionListener); + break; + case REVOKE: + handleRevokeAccess(request, actionListener); + break; + case VERIFY: + handleVerifyAccess(request, actionListener); + break; + default: + actionListener.onFailure(new IllegalArgumentException("Unknown action type: " + request.getOperation())); + } + } + + private void handleListResources(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.getAccessibleResourcesForCurrentUser( + request.getResourceIndex(), + ActionListener.wrap(resources -> listener.onResponse(new ResourceAccessResponse(resources)), listener::onFailure) + ); + } + + private void handleGrantAccess(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.shareWith( + request.getResourceId(), + request.getResourceIndex(), + request.getShareWith(), + ActionListener.wrap(response -> listener.onResponse(new ResourceAccessResponse(response)), listener::onFailure) + ); + } + + private void handleRevokeAccess(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.revokeAccess( + request.getResourceId(), + request.getResourceIndex(), + parseRevokedEntities(request.getRevokedEntities()), + request.getScopes(), + ActionListener.wrap(success -> listener.onResponse(new ResourceAccessResponse(success)), listener::onFailure) + ); + } + + private void handleVerifyAccess(ResourceAccessRequest request, ActionListener listener) { + resourceAccessHandler.hasPermission( + request.getResourceId(), + request.getResourceIndex(), + request.getScopes(), + ActionListener.wrap(hasPermission -> listener.onResponse(new ResourceAccessResponse(hasPermission)), listener::onFailure) + ); + } + + private Map> parseRevokedEntities(Map> revokeSource) { + return revokeSource.entrySet() + .stream() + .collect(Collectors.toMap(entry -> RecipientTypeRegistry.fromValue(entry.getKey()), Map.Entry::getValue)); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java b/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java new file mode 100644 index 0000000000..2aafb9898a --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java @@ -0,0 +1,404 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.common.support; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +import org.opensearch.common.settings.Settings; +import org.opensearch.security.common.auditlog.impl.AuditCategory; + +import com.password4j.types.Hmac; + +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 OPENSEARCH_SECURITY_DISABLED = SECURITY_SETTINGS_PREFIX + "disabled"; + public static final boolean OPENSEARCH_SECURITY_DISABLED_DEFAULT = false; + + public static final String OPENDISTRO_SECURITY_CHANNEL_TYPE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "channel_type"; + + public static final String OPENDISTRO_SECURITY_ORIGIN = OPENDISTRO_SECURITY_CONFIG_PREFIX + "origin"; + public static final String OPENDISTRO_SECURITY_ORIGIN_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "origin_header"; + + public static final String OPENDISTRO_SECURITY_DLS_QUERY_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_query"; + + public static final String OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "dls_filter_level_query"; + public static final String OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "dls_filter_level_query_t"; + + public static final String OPENDISTRO_SECURITY_DLS_MODE_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_mode"; + public static final String OPENDISTRO_SECURITY_DLS_MODE_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_mode_t"; + + public static final String OPENDISTRO_SECURITY_FLS_FIELDS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "fls_fields"; + + public static final String OPENDISTRO_SECURITY_MASKED_FIELD_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "masked_fields"; + + public static final String OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "doc_allowlist"; + public static final String OPENDISTRO_SECURITY_DOC_ALLOWLIST_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "doc_allowlist_t"; + + public static final String OPENDISTRO_SECURITY_FILTER_LEVEL_DLS_DONE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "filter_level_dls_done"; + + public static final String OPENDISTRO_SECURITY_DLS_QUERY_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_query_ccs"; + + public static final String OPENDISTRO_SECURITY_FLS_FIELDS_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "fls_fields_ccs"; + + public static final String OPENDISTRO_SECURITY_MASKED_FIELD_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "masked_fields_ccs"; + + public static final String OPENDISTRO_SECURITY_CONF_REQUEST_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "conf_request"; + + public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "remote_address"; + public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "remote_address_header"; + + public static final String OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "initial_action_class_header"; + + /** + * Set by SSL plugin for https requests only + */ + public static final String OPENDISTRO_SECURITY_SSL_PEER_CERTIFICATES = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_peer_certificates"; + + /** + * Set by SSL plugin for https requests only + */ + public static final String OPENDISTRO_SECURITY_SSL_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_principal"; + + /** + * If this is set to TRUE then the request comes from a Server Node (fully trust) + * Its expected that there is a _opendistro_security_user attached as header + */ + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_INTERCLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "ssl_transport_intercluster_request"; + + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "ssl_transport_trustedcluster_request"; + + // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_EXTENSION_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX + + "ssl_transport_extension_request"; + // CS-ENFORCE-SINGLE + + /** + * Set by the SSL plugin, this is the peer node certificate on the transport layer + */ + public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_transport_principal"; + + public static final String OPENDISTRO_SECURITY_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user"; + public static final String OPENDISTRO_SECURITY_USER_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_header"; + + // persistent header. This header is set once and cannot be stashed + public static final String OPENDISTRO_SECURITY_AUTHENTICATED_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "authenticated_user"; + + public static final String OPENDISTRO_SECURITY_INITIATING_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "_initiating_user"; + + public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; + + public static final String OPENDISTRO_SECURITY_INJECTED_USER = "injected_user"; + public static final String OPENDISTRO_SECURITY_INJECTED_USER_HEADER = "injected_user_header"; + + public static final String OPENDISTRO_SECURITY_XFF_DONE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "xff_done"; + + public static final String SSO_LOGOUT_URL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "sso_logout_url"; + + public static final String OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX = ".opendistro_security"; + + 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 = 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" + // "indices:data/write/bulk" + )) + ); + + 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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"; + public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT = "opendistro_security.audit.enable_transport"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES = + "opendistro_security.audit.config.disabled_transport_categories"; + public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES = + "opendistro_security.audit.config.disabled_rest_categories"; + public static final List OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT = ImmutableList.of( + AuditCategory.AUTHENTICATED.toString(), + AuditCategory.GRANTED_PRIVILEGES.toString() + ); + 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 = 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 = SECURITY_SETTINGS_PREFIX + "audit.config."; + + // Internal Opensearch data_stream + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_NAME = "data_stream.name"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_MANAGE = "data_stream.template.manage"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NAME = "data_stream.template.name"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NUMBER_OF_REPLICAS = "data_stream.template.number_of_replicas"; + public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NUMBER_OF_SHARDS = "data_stream.template.number_of_shards"; + + // Internal / External OpenSearch + public static final String SECURITY_AUDIT_OPENSEARCH_INDEX = "index"; + public static final String SECURITY_AUDIT_OPENSEARCH_TYPE = "type"; + + // External OpenSearch + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS = "http_endpoints"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME = "username"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD = "password"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL = "enable_ssl"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES = "verify_hostnames"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH = "enable_ssl_client_auth"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH = "pemkey_filepath"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_CONTENT = "pemkey_content"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD = "pemkey_password"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH = "pemcert_filepath"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_CONTENT = "pemcert_content"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH = "pemtrustedcas_filepath"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT = "pemtrustedcas_content"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS = "cert_alias"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_CIPHERS = "enabled_ssl_ciphers"; + public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_PROTOCOLS = "enabled_ssl_protocols"; + + // Webhooks + public static final String SECURITY_AUDIT_WEBHOOK_URL = "webhook.url"; + public static final String SECURITY_AUDIT_WEBHOOK_FORMAT = "webhook.format"; + public static final String SECURITY_AUDIT_WEBHOOK_SSL_VERIFY = "webhook.ssl.verify"; + public static final String SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH = "webhook.ssl.pemtrustedcas_filepath"; + public static final String SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT = "webhook.ssl.pemtrustedcas_content"; + + // Log4j + public static final String SECURITY_AUDIT_LOG4J_LOGGER_NAME = "log4j.logger_name"; + public static final String SECURITY_AUDIT_LOG4J_LEVEL = "log4j.level"; + + // retry + 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 = 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 = 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 = SECURITY_SETTINGS_PREFIX + + "allow_default_init_securityindex.use_cluster_state"; + + 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 = SECURITY_SETTINGS_PREFIX + "roles_mapping_resolution"; + + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY = + "opendistro_security.compliance.history.write.metadata_only"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY = + "opendistro_security.compliance.history.read.metadata_only"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS = + "opendistro_security.compliance.history.read.watched_fields"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES = + "opendistro_security.compliance.history.write.watched_indices"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS = + "opendistro_security.compliance.history.write.log_diffs"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS = + "opendistro_security.compliance.history.read.ignore_users"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS = + "opendistro_security.compliance.history.write.ignore_users"; + public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED = + "opendistro_security.compliance.history.external_config_enabled"; + public static final String OPENDISTRO_SECURITY_SOURCE_FIELD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "source_field_context"; + public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = 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 = 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 = 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, + BACKENDROLES_ONLY, + BOTH + } + + 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 = 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 = 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 = 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 = SECURITY_SETTINGS_PREFIX + "protected_indices.roles"; + public static final List SECURITY_PROTECTED_INDICES_ROLES_DEFAULT = Collections.emptyList(); + + // Roles injection for plugins + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES = "opendistro_security_injected_roles"; + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_HEADER = "opendistro_security_injected_roles_header"; + + // Roles validation for the plugins + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION = "opendistro_security_injected_roles_validation"; + public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION_HEADER = + "opendistro_security_injected_roles_validation_header"; + + // System indices settings + public static final String SYSTEM_INDEX_PERMISSION = "system:admin/system_index"; + 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 = 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 = 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 = 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 = SECURITY_SETTINGS_PREFIX + "use_jdk_serialization"; + + // On-behalf-of endpoints settings + // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings + public static final String EXTENSIONS_BWC_PLUGIN_MODE = "bwcPluginMode"; + public static final boolean EXTENSIONS_BWC_PLUGIN_MODE_DEFAULT = false; + // CS-ENFORCE-SINGLE + + // Variable for initial admin password support + public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD"; + + // Resource sharing feature-flag + public static final String OPENSEARCH_RESOURCE_SHARING_ENABLED = SECURITY_SETTINGS_PREFIX + "resource_sharing.enabled"; + public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = true; + + public static Set getSettingAsSet( + final Settings settings, + final String key, + final List defaultList, + final boolean ignoreCaseForNone + ) { + final List list = settings.getAsList(key, defaultList); + if (list.size() == 1 && "NONE".equals(ignoreCaseForNone ? list.get(0).toUpperCase() : list.get(0))) { + return Collections.emptySet(); + } + return ImmutableSet.copyOf(list); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/support/Utils.java b/common/src/main/java/org/opensearch/security/common/support/Utils.java new file mode 100644 index 0000000000..ffdc8d9390 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/support/Utils.java @@ -0,0 +1,285 @@ +/* + * 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.common.support; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.security.AccessController; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.tuple.Pair; + +import org.opensearch.ExceptionsHelper; +import org.opensearch.OpenSearchParseException; +import org.opensearch.SpecialPermission; +import org.opensearch.common.CheckedSupplier; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.common.Strings; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.common.transport.TransportAddress; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.NamedRoute; +import org.opensearch.rest.RestHandler.DeprecatedRoute; +import org.opensearch.rest.RestHandler.Route; +import org.opensearch.security.common.DefaultObjectMapper; +import org.opensearch.security.common.user.User; + +import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; + +public class Utils { + @Deprecated + public static final String LEGACY_OPENDISTRO_PREFIX = "_opendistro/_security"; + public static final String PLUGINS_PREFIX = "_plugins/_security"; + + public final static String PLUGIN_ROUTE_PREFIX = "/" + PLUGINS_PREFIX; + + @Deprecated + public final static String LEGACY_PLUGIN_ROUTE_PREFIX = "/" + LEGACY_OPENDISTRO_PREFIX; + + public final static String PLUGIN_API_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/api"; + + @Deprecated + public final static String LEGACY_PLUGIN_API_ROUTE_PREFIX = LEGACY_PLUGIN_ROUTE_PREFIX + "/api"; + + public final static String OPENDISTRO_API_DEPRECATION_MESSAGE = + "[_opendistro/_security] is a deprecated endpoint path. Please use _plugins/_security instead."; + + public final static String PLUGIN_RESOURCE_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/resources"; + + private static final ObjectMapper internalMapper = new ObjectMapper(); + + public static Map convertJsonToxToStructuredMap(ToXContent jsonContent) { + Map map = null; + try { + final BytesReference bytes = XContentHelper.toXContent(jsonContent, XContentType.JSON, false); + map = XContentHelper.convertToMap(bytes, false, XContentType.JSON).v2(); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + + return map; + } + + public static Map convertJsonToxToStructuredMap(String jsonContent) { + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, jsonContent) + ) { + return parser.map(); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + } + + private static BytesReference convertStructuredMapToBytes(Map structuredMap) { + try { + return BytesReference.bytes(JsonXContent.contentBuilder().map(structuredMap)); + } catch (IOException e) { + throw new OpenSearchParseException("Failed to convert map", e); + } + } + + public static String convertStructuredMapToJson(Map structuredMap) { + try { + return XContentHelper.convertToJson(convertStructuredMapToBytes(structuredMap), false, XContentType.JSON); + } catch (IOException e) { + throw new OpenSearchParseException("Failed to convert map", e); + } + } + + public static JsonNode convertJsonToJackson(BytesReference jsonContent) { + try { + return DefaultObjectMapper.readTree(jsonContent.utf8ToString()); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + + } + + public static JsonNode toJsonNode(final String content) throws IOException { + return DefaultObjectMapper.readTree(content); + } + + public static Object toConfigObject(final JsonNode content, final Class clazz) throws IOException { + return DefaultObjectMapper.readTree(content, clazz); + } + + public static JsonNode convertJsonToJackson(ToXContent jsonContent, boolean omitDefaults) { + try { + return DefaultObjectMapper.readTree( + Strings.toString( + XContentType.JSON, + jsonContent, + new ToXContent.MapParams(Map.of("omit_defaults", String.valueOf(omitDefaults))) + ) + ); + } catch (IOException e1) { + throw ExceptionsHelper.convertToOpenSearchException(e1); + } + + } + + @SuppressWarnings("removal") + public static byte[] jsonMapToByteArray(Map jsonAsMap) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged((PrivilegedExceptionAction) () -> internalMapper.writeValueAsBytes(jsonAsMap)); + } catch (final PrivilegedActionException e) { + if (e.getCause() instanceof JsonProcessingException) { + throw (JsonProcessingException) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + + @SuppressWarnings("removal") + public static Map byteArrayToMutableJsonMap(byte[] jsonBytes) throws IOException { + + final SecurityManager sm = System.getSecurityManager(); + + if (sm != null) { + sm.checkPermission(new SpecialPermission()); + } + + try { + return AccessController.doPrivileged( + (PrivilegedExceptionAction>) () -> internalMapper.readValue( + jsonBytes, + new TypeReference>() { + } + ) + ); + } catch (final PrivilegedActionException e) { + if (e.getCause() instanceof IOException) { + throw (IOException) e.getCause(); + } else if (e.getCause() instanceof RuntimeException) { + throw (RuntimeException) e.getCause(); + } else { + throw new RuntimeException(e.getCause()); + } + } + } + + /** + * Generate field resource paths + * @param fields fields + * @param prefix prefix path + * @return new set of fields resource paths + */ + public static Set generateFieldResourcePaths(final Set fields, final String prefix) { + return fields.stream().map(field -> prefix + field).collect(ImmutableSet.toImmutableSet()); + } + + /** + * Add prefixes(_plugins/_security/api) to rest API routes + * @param routes routes + * @return new list of API routes prefixed with and _plugins/_security/api + */ + public static List addRoutesPrefix(List routes) { + return addRoutesPrefix(routes, PLUGIN_API_ROUTE_PREFIX); + } + + /** + * Add prefixes(_opendistro/_security/api) to rest API routes + * Deprecated in favor of addRoutesPrefix(List routes) + * @param routes routes + * @return new list of API routes prefixed with and _opendistro/_security/api + */ + @Deprecated + public static List addLegacyRoutesPrefix(List routes) { + return addDeprecatedRoutesPrefix(routes, LEGACY_PLUGIN_API_ROUTE_PREFIX); + } + + /** + * Add customized prefix(_opendistro... and _plugins...)to API rest routes + * @param routes routes + * @param prefixes all api prefix + * @return new list of API routes prefixed with the strings listed in prefixes + * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in + */ + public static List addRoutesPrefix(List routes, final String... prefixes) { + return routes.stream().flatMap(r -> Arrays.stream(prefixes).map(p -> { + if (r instanceof NamedRoute) { + NamedRoute nr = (NamedRoute) r; + return new NamedRoute.Builder().method(nr.getMethod()) + .path(p + nr.getPath()) + .uniqueName(nr.name()) + .legacyActionNames(nr.actionNames()) + .build(); + } + return new Route(r.getMethod(), p + r.getPath()); + })).collect(ImmutableList.toImmutableList()); + } + + /** + * Add prefixes(_plugins...) to rest API routes + * @param deprecatedRoutes Routes being deprecated + * @return new list of API routes prefixed with _opendistro... and _plugins... + *Total number of routes is expanded as twice as the number of routes passed in + */ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes) { + return addDeprecatedRoutesPrefix(deprecatedRoutes, LEGACY_PLUGIN_API_ROUTE_PREFIX, PLUGIN_API_ROUTE_PREFIX); + } + + /** + * Add customized prefix(_opendistro... and _plugins...)to API rest routes + * @param deprecatedRoutes Routes being deprecated + * @param prefixes all api prefix + * @return new list of API routes prefixed with the strings listed in prefixes + * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in + */ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes, final String... prefixes) { + return deprecatedRoutes.stream() + .flatMap(r -> Arrays.stream(prefixes).map(p -> new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) + .collect(ImmutableList.toImmutableList()); + } + + public static Pair userAndRemoteAddressFrom(final ThreadContext threadContext) { + final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + return Pair.of(user, remoteAddress); + } + + public static T withIOException(final CheckedSupplier action) { + try { + return action.get(); + } catch (final IOException ioe) { + throw new UncheckedIOException(ioe); + } + } + +} diff --git a/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java b/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java new file mode 100644 index 0000000000..4e5ab5b29b --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java @@ -0,0 +1,556 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.common.support; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collector; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableSet; + +public abstract class WildcardMatcher implements Predicate { + + public static final WildcardMatcher ANY = new WildcardMatcher() { + + @Override + public boolean matchAny(Stream candidates) { + return true; + } + + @Override + public boolean matchAny(Collection candidates) { + return true; + } + + @Override + public boolean matchAny(String... candidates) { + return true; + } + + @Override + public boolean matchAll(Stream candidates) { + return true; + } + + @Override + public boolean matchAll(Collection candidates) { + return true; + } + + @Override + public boolean matchAll(String[] candidates) { + return true; + } + + @Override + public > T getMatchAny(Stream candidates, Collector collector) { + return candidates.collect(collector); + } + + @Override + public boolean test(String candidate) { + return true; + } + + @Override + public String toString() { + return "*"; + } + }; + + public static final WildcardMatcher NONE = new WildcardMatcher() { + + @Override + public boolean matchAny(Stream candidates) { + return false; + } + + @Override + public boolean matchAny(Collection candidates) { + return false; + } + + @Override + public boolean matchAny(String... candidates) { + return false; + } + + @Override + public boolean matchAll(Stream candidates) { + return false; + } + + @Override + public boolean matchAll(Collection candidates) { + return false; + } + + @Override + public boolean matchAll(String[] candidates) { + return false; + } + + @Override + public > T getMatchAny(Stream candidates, Collector collector) { + return Stream.empty().collect(collector); + } + + @Override + public > T getMatchAny(Collection candidate, Collector collector) { + return Stream.empty().collect(collector); + } + + @Override + public > T getMatchAny(String[] candidate, Collector collector) { + return Stream.empty().collect(collector); + } + + @Override + public boolean test(String candidate) { + return false; + } + + @Override + public String toString() { + return ""; + } + }; + + public static WildcardMatcher from(String pattern, boolean caseSensitive) { + if (pattern == null) { + return NONE; + } else if (pattern.equals("*")) { + return ANY; + } else if (pattern.startsWith("/") && pattern.endsWith("/")) { + return new RegexMatcher(pattern, caseSensitive); + } else if (pattern.indexOf('?') >= 0 || pattern.indexOf('*') >= 0) { + return caseSensitive ? new SimpleMatcher(pattern) : new CasefoldingMatcher(pattern, SimpleMatcher::new); + } else { + return caseSensitive ? new Exact(pattern) : new CasefoldingMatcher(pattern, Exact::new); + } + } + + public static WildcardMatcher from(String pattern) { + return from(pattern, true); + } + + // This may in future use more optimized techniques to combine multiple WildcardMatchers in a single automaton + public static WildcardMatcher from(Stream stream, boolean caseSensitive) { + Collection matchers = stream.map(t -> { + if (t == null) { + return NONE; + } else if (t instanceof String) { + return WildcardMatcher.from(((String) t), caseSensitive); + } else if (t instanceof WildcardMatcher) { + return ((WildcardMatcher) t); + } + throw new UnsupportedOperationException("WildcardMatcher can't be constructed from " + t.getClass().getSimpleName()); + }).collect(ImmutableSet.toImmutableSet()); + + if (matchers.isEmpty()) { + return NONE; + } else if (matchers.size() == 1) { + return matchers.stream().findFirst().get(); + } + return new MatcherCombiner(matchers); + } + + public static WildcardMatcher from(Collection collection, boolean caseSensitive) { + if (collection == null || collection.isEmpty()) { + return NONE; + } else if (collection.size() == 1) { + T t = collection.stream().findFirst().get(); + if (t instanceof String) { + return from(((String) t), caseSensitive); + } else if (t instanceof WildcardMatcher) { + return ((WildcardMatcher) t); + } + throw new UnsupportedOperationException("WildcardMatcher can't be constructed from " + t.getClass().getSimpleName()); + } + return from(collection.stream(), caseSensitive); + } + + public static WildcardMatcher from(String[] patterns, boolean caseSensitive) { + if (patterns == null || patterns.length == 0) { + return NONE; + } else if (patterns.length == 1) { + return from(patterns[0], caseSensitive); + } + return from(Arrays.stream(patterns), caseSensitive); + } + + public static WildcardMatcher from(Stream patterns) { + return from(patterns, true); + } + + public static WildcardMatcher from(Collection patterns) { + return from(patterns, true); + } + + public static WildcardMatcher from(String... patterns) { + return from(patterns, true); + } + + public WildcardMatcher concat(Stream matchers) { + return new MatcherCombiner(Stream.concat(matchers, Stream.of(this)).collect(ImmutableSet.toImmutableSet())); + } + + public WildcardMatcher concat(Collection matchers) { + if (matchers.isEmpty()) { + return this; + } + return concat(matchers.stream()); + } + + public WildcardMatcher concat(WildcardMatcher... matchers) { + if (matchers.length == 0) { + return this; + } + return concat(Arrays.stream(matchers)); + } + + public boolean matchAny(Stream candidates) { + return candidates.anyMatch(this); + } + + public boolean matchAny(Collection candidates) { + return matchAny(candidates.stream()); + } + + public boolean matchAny(String... candidates) { + return matchAny(Arrays.stream(candidates)); + } + + public boolean matchAll(Stream candidates) { + return candidates.allMatch(this); + } + + public boolean matchAll(Collection candidates) { + return matchAll(candidates.stream()); + } + + public boolean matchAll(String[] candidates) { + return matchAll(Arrays.stream(candidates)); + } + + public > T getMatchAny(Stream candidates, Collector collector) { + return candidates.filter(this).collect(collector); + } + + public > T getMatchAny(Collection candidate, Collector collector) { + return getMatchAny(candidate.stream(), collector); + } + + public > T getMatchAny(final String[] candidate, Collector collector) { + return getMatchAny(Arrays.stream(candidate), collector); + } + + public Optional findFirst(final String candidate) { + return Optional.ofNullable(test(candidate) ? this : null); + } + + public Iterable iterateMatching(Iterable candidates) { + return iterateMatching(candidates, Function.identity()); + } + + public Iterable iterateMatching(Iterable candidates, Function toStringFunction) { + return new Iterable() { + + @Override + public Iterator iterator() { + Iterator delegate = candidates.iterator(); + + return new Iterator() { + private E next; + + @Override + public boolean hasNext() { + if (next == null) { + init(); + } + + return next != null; + } + + @Override + public E next() { + if (next == null) { + init(); + } + + E result = next; + next = null; + return result; + } + + private void init() { + while (delegate.hasNext()) { + E candidate = delegate.next(); + + if (test(toStringFunction.apply(candidate))) { + next = candidate; + break; + } + } + } + }; + } + }; + } + + public static List matchers(Collection patterns) { + return patterns.stream().map(p -> WildcardMatcher.from(p, true)).collect(Collectors.toList()); + } + + public static List getAllMatchingPatterns(final Collection matchers, final String candidate) { + return matchers.stream().filter(p -> p.test(candidate)).map(Objects::toString).collect(Collectors.toList()); + } + + public static List getAllMatchingPatterns(final Collection pattern, final Collection candidates) { + return pattern.stream().filter(p -> p.matchAny(candidates)).map(Objects::toString).collect(Collectors.toList()); + } + + public static boolean isExact(String pattern) { + return pattern == null || !(pattern.contains("*") || pattern.contains("?") || (pattern.startsWith("/") && pattern.endsWith("/"))); + } + + // + // --- Implementation specializations --- + // + // Casefolding matcher - sits on top of case-sensitive matcher + // and proxies toLower() of input string to the wrapped matcher + private static final class CasefoldingMatcher extends WildcardMatcher { + + private final WildcardMatcher inner; + + public CasefoldingMatcher(String pattern, Function simpleWildcardMatcher) { + this.inner = simpleWildcardMatcher.apply(pattern.toLowerCase()); + } + + @Override + public boolean test(String candidate) { + return inner.test(candidate.toLowerCase()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CasefoldingMatcher that = (CasefoldingMatcher) o; + return inner.equals(that.inner); + } + + @Override + public int hashCode() { + return inner.hashCode(); + } + + @Override + public String toString() { + return inner.toString(); + } + } + + public static final class Exact extends WildcardMatcher { + + private final String pattern; + + private Exact(String pattern) { + this.pattern = pattern; + } + + @Override + public boolean test(String candidate) { + return pattern.equals(candidate); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Exact that = (Exact) o; + return pattern.equals(that.pattern); + } + + @Override + public int hashCode() { + return pattern.hashCode(); + } + + @Override + public String toString() { + return pattern; + } + } + + // RegexMatcher uses JDK Pattern to test for matching, + // assumes "//" strings as input pattern + private static final class RegexMatcher extends WildcardMatcher { + + private final Pattern pattern; + + private RegexMatcher(String pattern, boolean caseSensitive) { + Preconditions.checkArgument(pattern.length() > 1 && pattern.startsWith("/") && pattern.endsWith("/")); + final String stripSlashesPattern = pattern.substring(1, pattern.length() - 1); + this.pattern = caseSensitive + ? Pattern.compile(stripSlashesPattern) + : Pattern.compile(stripSlashesPattern, Pattern.CASE_INSENSITIVE); + } + + @Override + public boolean test(String candidate) { + return pattern.matcher(candidate).matches(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RegexMatcher that = (RegexMatcher) o; + return pattern.pattern().equals(that.pattern.pattern()); + } + + @Override + public int hashCode() { + return pattern.pattern().hashCode(); + } + + @Override + public String toString() { + return "/" + pattern.pattern() + "/"; + } + } + + // Simple implementation of WildcardMatcher matcher with * and ? without + // using exlicit stack or recursion (as long as we don't need sub-matches it does work) + // allows us to save on resources and heap allocations unless Regex is required + private static final class SimpleMatcher extends WildcardMatcher { + + private final String pattern; + + SimpleMatcher(String pattern) { + this.pattern = pattern; + } + + @Override + public boolean test(String candidate) { + int i = 0; + int j = 0; + int n = candidate.length(); + int m = pattern.length(); + int text_backup = -1; + int wild_backup = -1; + while (i < n) { + if (j < m && pattern.charAt(j) == '*') { + text_backup = i; + wild_backup = ++j; + } else if (j < m && (pattern.charAt(j) == '?' || pattern.charAt(j) == candidate.charAt(i))) { + i++; + j++; + } else { + if (wild_backup == -1) return false; + i = ++text_backup; + j = wild_backup; + } + } + while (j < m && pattern.charAt(j) == '*') + j++; + return j >= m; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SimpleMatcher that = (SimpleMatcher) o; + return pattern.equals(that.pattern); + } + + @Override + public int hashCode() { + return pattern.hashCode(); + } + + @Override + public String toString() { + return pattern; + } + } + + // MatcherCombiner is a combination of a set of matchers + // matches if any of the set do + // Empty MultiMatcher always returns false + private static final class MatcherCombiner extends WildcardMatcher { + + private final Collection wildcardMatchers; + private final int hashCode; + + MatcherCombiner(Collection wildcardMatchers) { + Preconditions.checkArgument(wildcardMatchers.size() > 1); + this.wildcardMatchers = wildcardMatchers; + hashCode = wildcardMatchers.hashCode(); + } + + @Override + public boolean test(String candidate) { + return wildcardMatchers.stream().anyMatch(m -> m.test(candidate)); + } + + @Override + public Optional findFirst(final String candidate) { + return wildcardMatchers.stream().filter(m -> m.test(candidate)).findFirst(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MatcherCombiner that = (MatcherCombiner) o; + return wildcardMatchers.equals(that.wildcardMatchers); + } + + @Override + public int hashCode() { + return hashCode; + } + + @Override + public String toString() { + return wildcardMatchers.toString(); + } + } +} diff --git a/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java b/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java new file mode 100644 index 0000000000..9255b63dba --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java @@ -0,0 +1,254 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.common.user; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.opensearch.OpenSearchSecurityException; + +/** + * AuthCredentials are an abstraction to encapsulate credentials like passwords or generic + * native credentials like GSS tokens. + * + */ +public final class AuthCredentials { + + private static final String DIGEST_ALGORITHM = "SHA-256"; + private final String username; + private byte[] password; + private Object nativeCredentials; + private final Set securityRoles = new HashSet(); + private final Set backendRoles = new HashSet(); + private boolean complete; + private final byte[] internalPasswordHash; + private final Map attributes = new HashMap<>(); + + /** + * Create new credentials with a username and native credentials + * + * @param username The username, must not be null or empty + * @param nativeCredentials Arbitrary credentials (like GSS tokens), must not be null + * @throws IllegalArgumentException if username or nativeCredentials are null or empty + */ + public AuthCredentials(final String username, final Object nativeCredentials) { + this(username, null, nativeCredentials); + + if (nativeCredentials == null) { + throw new IllegalArgumentException("nativeCredentials must not be null or empty"); + } + } + + /** + * Create new credentials with a username and password + * + * @param username The username, must not be null or empty + * @param password The password, must not be null or empty + * @throws IllegalArgumentException if username or password is null or empty + */ + public AuthCredentials(final String username, final byte[] password) { + this(username, password, null); + + if (password == null || password.length == 0) { + throw new IllegalArgumentException("password must not be null or empty"); + } + } + + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + + * @param username The username, must not be null or empty + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, String... backendRoles) { + this(username, null, null, backendRoles); + } + + /** + * Create new credentials with a username, a initial optional set of roles and empty password/native credentials + * @param username The username, must not be null or empty + * @param securityRoles The internal roles the user has been mapped to + * @param backendRoles set of roles this user is a member of + * @throws IllegalArgumentException if username is null or empty + */ + public AuthCredentials(final String username, List securityRoles, String... backendRoles) { + this(username, null, null, backendRoles); + this.securityRoles.addAll(securityRoles); + } + + private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { + super(); + + if (username == null || username.isEmpty()) { + throw new IllegalArgumentException("username must not be null or empty"); + } + + this.username = username; + // make defensive copy + this.password = password == null ? null : Arrays.copyOf(password, password.length); + + if (this.password != null) { + try { + MessageDigest digester = MessageDigest.getInstance(DIGEST_ALGORITHM); + internalPasswordHash = digester.digest(this.password); + } catch (NoSuchAlgorithmException e) { + throw new OpenSearchSecurityException("Unable to digest password", e); + } + } else { + internalPasswordHash = null; + } + + if (password != null) { + Arrays.fill(password, (byte) '\0'); + password = null; + } + + this.nativeCredentials = nativeCredentials; + nativeCredentials = null; + + if (backendRoles != null && backendRoles.length > 0) { + this.backendRoles.addAll(Arrays.asList(backendRoles)); + } + } + + /** + * Wipe password and native credentials + */ + public void clearSecrets() { + if (password != null) { + Arrays.fill(password, (byte) '\0'); + password = null; + } + + nativeCredentials = null; + } + + public String getUsername() { + return username; + } + + /** + * + * @return Defensive copy of the password + */ + public byte[] getPassword() { + // make defensive copy + return password == null ? null : Arrays.copyOf(password, password.length); + } + + public Object getNativeCredentials() { + return nativeCredentials; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + Arrays.hashCode(internalPasswordHash); + result = prime * result + ((username == null) ? 0 : username.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + AuthCredentials other = (AuthCredentials) obj; + if (internalPasswordHash == null + || other.internalPasswordHash == null + || !MessageDigest.isEqual(internalPasswordHash, other.internalPasswordHash)) return false; + if (username == null) { + if (other.username != null) return false; + } else if (!username.equals(other.username)) return false; + return true; + } + + @Override + public String toString() { + return "AuthCredentials [username=" + + username + + ", password empty=" + + (password == null) + + ", nativeCredentials empty=" + + (nativeCredentials == null) + + ",backendRoles=" + + backendRoles + + "]"; + } + + /** + * + * @return Defensive copy of the roles this user is member of. + */ + public Set getBackendRoles() { + return new HashSet(backendRoles); + } + + /** + * + * @return Defensive copy of the security roles this user is member of. + */ + public Set getSecurityRoles() { + return Set.copyOf(securityRoles); + } + + public boolean isComplete() { + return complete; + } + + /** + * If the credentials are complete and no further roundtrips with the originator are due + * then this method must be called so that the authentication flow can proceed. + *

+ * If this credentials are already marked a complete then a call to this method does nothing. + * + * @return this + */ + public AuthCredentials markComplete() { + this.complete = true; + return this; + } + + public void addAttribute(String name, String value) { + if (name != null && !name.isEmpty()) { + this.attributes.put(name, value); + } + } + + public Map getAttributes() { + return Collections.unmodifiableMap(this.attributes); + } +} diff --git a/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java b/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java new file mode 100644 index 0000000000..144bb04002 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java @@ -0,0 +1,34 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.common.user; + +import java.util.Map; + +public interface CustomAttributesAware { + + Map getCustomAttributesMap(); +} diff --git a/common/src/main/java/org/opensearch/security/common/user/User.java b/common/src/main/java/org/opensearch/security/common/user/User.java new file mode 100644 index 0000000000..015ddf7fb1 --- /dev/null +++ b/common/src/main/java/org/opensearch/security/common/user/User.java @@ -0,0 +1,312 @@ +/* + * Copyright 2015-2018 _floragunn_ GmbH + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +package org.opensearch.security.common.user; + +import java.io.IOException; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import com.google.common.collect.Lists; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; + +/** + * A authenticated user and attributes associated to them (like roles, tenant, custom attributes) + *

+ * Do not subclass from this class! + */ +public class User implements Serializable, Writeable, CustomAttributesAware { + + public static final User ANONYMOUS = new User( + "opendistro_security_anonymous", + Lists.newArrayList("opendistro_security_anonymous_backendrole"), + null + ); + + // This is a default user that is injected into a transport request when a user info is not present and passive_intertransport_auth is + // enabled. + // This is to be used in scenarios where some of the nodes do not have security enabled, and therefore do not pass any user information + // in threadcontext, yet we need the communication to not break between the nodes. + // Attach the required permissions to either the user or the backend role. + public static final User DEFAULT_TRANSPORT_USER = new User( + "opendistro_security_default_transport_user", + Lists.newArrayList("opendistro_security_default_transport_backendrole"), + null + ); + + private static final long serialVersionUID = -5500938501822658596L; + private final String name; + /** + * roles == backend_roles + */ + private final Set roles = Collections.synchronizedSet(new HashSet()); + private final Set securityRoles = Collections.synchronizedSet(new HashSet()); + private String requestedTenant; + private Map attributes = Collections.synchronizedMap(new HashMap<>()); + private boolean isInjected = false; + + public User(final StreamInput in) throws IOException { + super(); + name = in.readString(); + roles.addAll(in.readList(StreamInput::readString)); + requestedTenant = in.readString(); + if (requestedTenant.isEmpty()) { + requestedTenant = null; + } + attributes = Collections.synchronizedMap(in.readMap(StreamInput::readString, StreamInput::readString)); + securityRoles.addAll(in.readList(StreamInput::readString)); + } + + /** + * Create a new authenticated user + * + * @param name The username (must not be null or empty) + * @param roles Roles of which the user is a member off (maybe null) + * @param customAttributes Custom attributes associated with this (maybe null) + * @throws IllegalArgumentException if name is null or empty + */ + public User(final String name, final Collection roles, final AuthCredentials customAttributes) { + super(); + + if (name == null || name.isEmpty()) { + throw new IllegalArgumentException("name must not be null or empty"); + } + + this.name = name; + + if (roles != null) { + this.addRoles(roles); + } + + if (customAttributes != null) { + this.attributes.putAll(customAttributes.getAttributes()); + } + + } + + /** + * Create a new authenticated user without roles and attributes + * + * @param name The username (must not be null or empty) + * @throws IllegalArgumentException if name is null or empty + */ + public User(final String name) { + this(name, null, null); + } + + public final String getName() { + return name; + } + + /** + * @return A unmodifiable set of the backend roles this user is a member of + */ + public final Set getRoles() { + return Collections.unmodifiableSet(roles); + } + + /** + * Associate this user with a backend role + * + * @param role The backend role + */ + public final void addRole(final String role) { + this.roles.add(role); + } + + /** + * Associate this user with a set of backend roles + * + * @param roles The backend roles + */ + public final void addRoles(final Collection roles) { + if (roles != null) { + this.roles.addAll(roles); + } + } + + /** + * Check if this user is a member of a backend role + * + * @param role The backend role + * @return true if this user is a member of the backend role, false otherwise + */ + public final boolean isUserInRole(final String role) { + return this.roles.contains(role); + } + + /** + * Associate this user with a set of custom attributes + * + * @param attributes custom attributes + */ + public final void addAttributes(final Map attributes) { + if (attributes != null) { + this.attributes.putAll(attributes); + } + } + + public final String getRequestedTenant() { + return requestedTenant; + } + + public final void setRequestedTenant(String requestedTenant) { + this.requestedTenant = requestedTenant; + } + + public boolean isInjected() { + return isInjected; + } + + public void setInjected(boolean isInjected) { + this.isInjected = isInjected; + } + + public final String toStringWithAttributes() { + return "User [name=" + + name + + ", backend_roles=" + + roles + + ", requestedTenant=" + + requestedTenant + + ", attributes=" + + attributes + + "]"; + } + + @Override + public final String toString() { + return "User [name=" + name + ", backend_roles=" + roles + ", requestedTenant=" + requestedTenant + "]"; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + (name == null ? 0 : name.hashCode()); + return result; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof User)) { + return false; + } + final User other = (User) obj; + if (name == null) { + if (other.name != null) { + return false; + } + } else if (!name.equals(other.name)) { + return false; + } + return true; + } + + /** + * Copy all backend roles from another user + * + * @param user The user from which the backend roles should be copied over + */ + public final void copyRolesFrom(final User user) { + if (user != null) { + this.addRoles(user.getRoles()); + } + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeStringCollection(new ArrayList(roles)); + out.writeString(requestedTenant == null ? "" : requestedTenant); + out.writeMap(attributes, StreamOutput::writeString, StreamOutput::writeString); + out.writeStringCollection(securityRoles == null ? Collections.emptyList() : new ArrayList(securityRoles)); + } + + /** + * Get the custom attributes associated with this user + * + * @return A modifiable map with all the current custom attributes associated with this user + */ + public synchronized final Map getCustomAttributesMap() { + if (attributes == null) { + attributes = Collections.synchronizedMap(new HashMap<>()); + } + return attributes; + } + + public final void addSecurityRoles(final Collection securityRoles) { + if (securityRoles != null && this.securityRoles != null) { + this.securityRoles.addAll(securityRoles); + } + } + + public final Set getSecurityRoles() { + return this.securityRoles == null + ? Collections.synchronizedSet(Collections.emptySet()) + : Collections.unmodifiableSet(this.securityRoles); + } + + /** + * Check the custom attributes associated with this user + * + * @return true if it has a service account attributes, otherwise false + */ + public boolean isServiceAccount() { + Map userAttributesMap = this.getCustomAttributesMap(); + return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service")); + } + + /** + * Check the custom attributes associated with this user + * + * @return true if it has a plugin account attributes, otherwise false + */ + public boolean isPluginUser() { + return name != null && name.startsWith("plugin:"); + } + + public void setAttributes(Map attributes) { + if (attributes == null) { + this.attributes = Collections.synchronizedMap(new HashMap<>()); + } + } +} diff --git a/scripts/build.sh b/scripts/build.sh index c4476731f5..e0fa495845 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -79,6 +79,8 @@ cp ${distributions}/*.zip ./$OUTPUT/plugins # Publish jars ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER +./gradlew :opensearch-security-common:publishToMavenLocal -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER +./gradlew :opensearch-security-client: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 diff --git a/settings.gradle b/settings.gradle index 193587dee7..09be0187cc 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,3 +8,9 @@ rootProject.name = 'opensearch-security' include "spi" project(":spi").name = "opensearch-resource-sharing-spi" + +include 'common' +project(":common").name = rootProject.name + "-common" + +include 'client' +project(":client").name = rootProject.name + "-client" diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 843553d971..56fda88a42 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -144,6 +144,16 @@ import org.opensearch.security.auditlog.config.AuditConfig.Filter.FilterEntries; import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; +import org.opensearch.security.common.resources.ResourceAccessHandler; +import org.opensearch.security.common.resources.ResourceIndexListener; +import org.opensearch.security.common.resources.ResourcePluginInfo; +import org.opensearch.security.common.resources.ResourceProvider; +import org.opensearch.security.common.resources.ResourceSharingConstants; +import org.opensearch.security.common.resources.ResourceSharingIndexHandler; +import org.opensearch.security.common.resources.ResourceSharingIndexManagementRepository; +import org.opensearch.security.common.resources.rest.ResourceAccessAction; +import org.opensearch.security.common.resources.rest.ResourceAccessRestAction; +import org.opensearch.security.common.resources.rest.ResourceAccessTransportAction; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; import org.opensearch.security.configuration.AdminDNs; @@ -184,6 +194,9 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.setting.TransportPassiveAuthSetting; +import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.ResourceSharingExtension; import org.opensearch.security.ssl.ExternalSecurityKeyStore; import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory; import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; @@ -260,6 +273,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; + private volatile org.opensearch.security.common.configuration.AdminDNs adminDNsCommon; private volatile ClusterService cs; private volatile AtomicReference localNode = new AtomicReference<>(); private volatile AuditLog auditLog; @@ -277,6 +291,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile OpensearchDynamicSetting transportPassiveAuthSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; + private ResourceSharingIndexManagementRepository rmr; public static boolean isActionTraceEnabled() { @@ -667,6 +682,14 @@ public List getRestHandlers( passwordHasher ) ); + + // Adds rest handlers for resource-access-control actions + if (settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + )) { + handlers.add(new ResourceAccessRestAction()); + } log.debug("Added {} rest handler(s)", handlers.size()); } } @@ -694,6 +717,12 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class)); } actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); + if (settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + )) { + actions.add(new ActionHandler<>(ResourceAccessAction.INSTANCE, ResourceAccessTransportAction.class)); + } } return actions; } @@ -721,6 +750,18 @@ public void onIndexModule(IndexModule indexModule) { dlsFlsBaseContext ) ); + + // Listening on POST and DELETE operations in resource indices + ResourceIndexListener resourceIndexListener = ResourceIndexListener.getInstance(); + resourceIndexListener.initialize(threadPool, localClient); + if (settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + ) && ResourcePluginInfo.getInstance().getResourceIndices().contains(indexModule.getIndex().getName())) { + indexModule.addIndexOperationListener(resourceIndexListener); + log.info("Security plugin started listening to operations on resource-index {}", indexModule.getIndex().getName()); + } + indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @Override @@ -1097,6 +1138,7 @@ public Collection createComponents( sslExceptionHandler = new AuditLogSslExceptionHandler(auditLog); adminDns = new AdminDNs(settings); + adminDNsCommon = new org.opensearch.security.common.configuration.AdminDNs(settings); cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); @@ -1125,6 +1167,17 @@ public Collection createComponents( namedXContentRegistry.get() ); + final var resourceSharingIndex = ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; + ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); + ResourceAccessHandler resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDNsCommon); + resourceAccessHandler.initializeRecipientTypes(); + // Resource Sharing index is enabled by default + boolean isResourceSharingEnabled = settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + ); + rmr = ResourceSharingIndexManagementRepository.create(rsIndexHandler, isResourceSharingEnabled); + dlsFlsBaseContext = new DlsFlsBaseContext(evaluator, threadPool.getThreadContext(), adminDns); if (SSLConfig.isSslOnlyMode()) { @@ -1205,6 +1258,7 @@ public Collection createComponents( } components.add(adminDns); + components.add(adminDNsCommon); components.add(cr); components.add(xffResolver); components.add(backendRegistry); @@ -1214,6 +1268,7 @@ public Collection createComponents( components.add(dcf); components.add(userService); components.add(passwordHasher); + components.add(resourceAccessHandler); components.add(sslSettingsManager); if (isSslCertReloadEnabled(settings) && sslCertificatesHotReloadEnabled(settings)) { @@ -2076,6 +2131,16 @@ public List> getSettings() { // Privileges evaluation settings.add(ActionPrivileges.PRECOMPUTED_PRIVILEGES_MAX_HEAP_SIZE); + + // Resource Sharing + settings.add( + Setting.boolSetting( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT, + Property.NodeScope, + Property.Filtered + ) + ); } return settings; @@ -2099,6 +2164,18 @@ public void onNodeStarted(DiscoveryNode localNode) { if (!SSLConfig.isSslOnlyMode() && !client && !disabled && !useClusterStateToInitSecurityConfig(settings)) { cr.initOnNodeStart(); } + + // rmr will be null when sec plugin is disabled or is in SSLOnly mode, hence rmr will not be instantiated + if (settings != null + && settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + ) + && rmr != null) { + // create resource sharing index if absent + rmr.createResourceSharingIndexIfAbsent(); + } + final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } @@ -2144,7 +2221,11 @@ public Collection getSystemIndexDescriptors(Settings sett ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); final SystemIndexDescriptor securityIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); - return List.of(securityIndexDescriptor); + final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( + ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX, + "Resource Sharing index" + ); + return List.of(securityIndexDescriptor, resourceSharingIndexDescriptor); } @Override @@ -2206,7 +2287,27 @@ private void tryAddSecurityProvider() { // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings @Override public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { - // Resource Sharing extensions will be loaded here + + if (settings.getAsBoolean( + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, + ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT + )) { + Set resourceIndices = new HashSet<>(); + Map resourceProviders = new HashMap<>(); + for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) { + String resourceType = extension.getResourceType(); + String resourceIndexName = extension.getResourceIndex(); + ResourceParser resourceParser = extension.getResourceParser(); + + resourceIndices.add(resourceIndexName); + + ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser); + resourceProviders.put(resourceIndexName, resourceProvider); + log.info("Loaded resource sharing extension: {}, index: {}", resourceType, resourceIndexName); + } + ResourcePluginInfo.getInstance().setResourceIndices(resourceIndices); + ResourcePluginInfo.getInstance().setResourceProviders(resourceProviders); + } } // CS-ENFORCE-SINGLE diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 8d47017664..a8295a410f 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -58,6 +58,7 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; +import org.opensearch.security.common.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestChannel; @@ -197,7 +198,7 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { * @param request * @return The authenticated user, null means another roundtrip * @throws OpenSearchSecurityException - */ + */ public boolean authenticate(final SecurityRequestChannel request) { final boolean isDebugEnabled = log.isDebugEnabled(); final boolean isBlockedBasedOnAddress = request.getRemoteAddress() @@ -224,7 +225,7 @@ public boolean authenticate(final SecurityRequestChannel request) { if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call User superuser = new User(sslPrincipal); - UserSubject subject = new UserSubjectImpl(threadPool, superuser); + UserSubject subject = new UserSubjectImpl(threadPool, new org.opensearch.security.common.user.User(sslPrincipal)); threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); return true; @@ -391,8 +392,15 @@ public boolean authenticate(final SecurityRequestChannel request) { final User impersonatedUser = impersonate(request, authenticatedUser); final User effectiveUser = impersonatedUser == null ? authenticatedUser : impersonatedUser; threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, effectiveUser); - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_INITIATING_USER, authenticatedUser.getName()); - UserSubject subject = new UserSubjectImpl(threadPool, effectiveUser); + + // TODO: The following artistry must be reverted when User class is completely moved to :opensearch-security-common + org.opensearch.security.common.user.User effUser = new org.opensearch.security.common.user.User( + effectiveUser.getName(), + effectiveUser.getRoles(), + null + ); + effUser.setAttributes(effectiveUser.getCustomAttributesMap()); + UserSubject subject = new UserSubjectImpl(threadPool, effUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); } else { if (isDebugEnabled) { @@ -420,7 +428,14 @@ public boolean authenticate(final SecurityRequestChannel request) { User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); - UserSubject subject = new UserSubjectImpl(threadPool, anonymousUser); + org.opensearch.security.common.user.User anonymousUserCommon = new org.opensearch.security.common.user.User( + User.ANONYMOUS.getName(), + new HashSet<>(User.ANONYMOUS.getRoles()), + null + ); + anonymousUserCommon.setRequestedTenant(tenant); + + UserSubject subject = new UserSubjectImpl(threadPool, anonymousUserCommon); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 81b813e8b1..58d6f77d0b 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -69,6 +69,8 @@ public class Utils { public final static String OPENDISTRO_API_DEPRECATION_MESSAGE = "[_opendistro/_security] is a deprecated endpoint path. Please use _plugins/_security instead."; + public final static String PLUGIN_RESOURCE_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/resources"; + private static final ObjectMapper internalMapper = new ObjectMapper(); public static Map convertJsonToxToStructuredMap(ToXContent jsonContent) { diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 633b85cff6..346437e775 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -382,6 +382,10 @@ public enum RolesMappingResolution { // Variable for initial admin password support public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD"; + // Resource sharing feature-flag + public static final String OPENSEARCH_RESOURCE_SHARING_ENABLED = SECURITY_SETTINGS_PREFIX + "resource_sharing.enabled"; + public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = true; + public static Set getSettingAsSet( final Settings settings, final String key, diff --git a/src/test/java/org/opensearch/security/IndexIntegrationTests.java b/src/test/java/org/opensearch/security/IndexIntegrationTests.java index 31db353c63..91a92ab97d 100644 --- a/src/test/java/org/opensearch/security/IndexIntegrationTests.java +++ b/src/test/java/org/opensearch/security/IndexIntegrationTests.java @@ -846,19 +846,16 @@ public void testIndexResolveMinus() throws Exception { resc = rh.executeGetRequest("/*,-foo*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); - resc = rh.executeGetRequest("/*,-*security/_search", encodeBasicHeader("foo_all", "nagilum")); + resc = rh.executeGetRequest("/*,-*security,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - resc = rh.executeGetRequest("/*,-*security/_search", encodeBasicHeader("foo_all", "nagilum")); + resc = rh.executeGetRequest("/*,-*security,-foo*,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - resc = rh.executeGetRequest("/*,-*security,-foo*/_search", encodeBasicHeader("foo_all", "nagilum")); - assertThat(resc.getStatusCode(), is(HttpStatus.SC_OK)); - - resc = rh.executeGetRequest("/_all,-*security/_search", encodeBasicHeader("foo_all", "nagilum")); + resc = rh.executeGetRequest("/_all,-*security,-*resource*/_search", encodeBasicHeader("foo_all", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_FORBIDDEN)); - resc = rh.executeGetRequest("/_all,-*security/_search", encodeBasicHeader("nagilum", "nagilum")); + resc = rh.executeGetRequest("/_all,-*security,-*resource*/_search", encodeBasicHeader("nagilum", "nagilum")); assertThat(resc.getStatusCode(), is(HttpStatus.SC_BAD_REQUEST)); } diff --git a/src/test/java/org/opensearch/security/SlowIntegrationTests.java b/src/test/java/org/opensearch/security/SlowIntegrationTests.java index 74e3bfa9e4..99ac9fb1b9 100644 --- a/src/test/java/org/opensearch/security/SlowIntegrationTests.java +++ b/src/test/java/org/opensearch/security/SlowIntegrationTests.java @@ -66,6 +66,7 @@ public void testCustomInterclusterRequestEvaluator() throws Exception { ConfigConstants.SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, "org.opensearch.security.AlwaysFalseInterClusterRequestEvaluator" ) + .put(ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, false) .put("discovery.initial_state_timeout", "8s") .build(); setup(Settings.EMPTY, null, settings, false, ClusterConfiguration.DEFAULT, 5, 1); diff --git a/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java b/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java index 9e630ef750..07bac9e349 100644 --- a/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java +++ b/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java @@ -15,7 +15,8 @@ import org.junit.Test; -import org.opensearch.security.user.User; +import org.opensearch.security.common.auth.UserSubjectImpl; +import org.opensearch.security.common.user.User; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; From cdd725299072d0c9fdd602955967a9a15ffcccc5 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Tue, 18 Mar 2025 11:26:57 -0400 Subject: [PATCH 3/7] Updates client to handle security disabled scenario Signed-off-by: Darshit Chanpura --- .../client/resources/ResourceSharingNodeClient.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index 239e23e128..cbdbabc681 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -45,10 +45,9 @@ public ResourceSharingNodeClient(Client client, Settings settings) { ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT ); - this.isSecurityDisabled = settings.getAsBoolean( - ConfigConstants.OPENSEARCH_SECURITY_DISABLED, - ConfigConstants.OPENSEARCH_SECURITY_DISABLED_DEFAULT - ); + Settings securitySettings = settings.getAsSettings(ConfigConstants.SECURITY_SETTINGS_PREFIX); + this.isSecurityDisabled = securitySettings.isEmpty() + || settings.getAsBoolean(ConfigConstants.OPENSEARCH_SECURITY_DISABLED, ConfigConstants.OPENSEARCH_SECURITY_DISABLED_DEFAULT); } /** From 450fcc68a673019bcf6a9edae3687c60d6a7e8d8 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 19 Mar 2025 16:04:50 -0400 Subject: [PATCH 4/7] Updates build.gradle files and jarhell references Signed-off-by: Darshit Chanpura --- RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md | 6 +++++- client/README.md | 4 ---- client/build.gradle | 9 ++++++++- common/build.gradle | 8 +++++++- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md index 42c0c61731..c537f6a292 100644 --- a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md +++ b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md @@ -31,10 +31,14 @@ This feature introduces **two primary components** for plugin developers: ### **Plugin Implementation Requirements** Each plugin must: -- **Declare a dependency** on `opensearch-security-client` package: +- **Declare an `implementation` dependency** on `opensearch-security-client` package: ```build.gradle implementation group: 'org.opensearch', name:'opensearch-security-client', version: "${opensearch_build}" ``` +- **Declare a `compileOnly` dependency** on `opensearch-resource-sharing-spi` package: +```build.gradle +compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" +``` - **Extend** `opensearch-security` plugin with optional flag: ```build.gradle opensearchplugin { diff --git a/client/README.md b/client/README.md index 2f944adb35..b37e9ace93 100644 --- a/client/README.md +++ b/client/README.md @@ -1,7 +1,3 @@ -Here's a **refined and corrected** version of your `README.md` file with improved clarity, grammar, and formatting: - ---- - # **Resource Sharing Client** This package provides a **ResourceSharing client** that resource plugins can use to **implement access control** by communicating with the **OpenSearch Security Plugin**. diff --git a/client/build.gradle b/client/build.gradle index 8bef3910bc..9923979f24 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -35,7 +35,12 @@ repositories { dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" // SPI dependency comes through common - implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') + compileOnly project(path: ":${rootProject.name}-common", configuration: 'shadow') + compileOnly project(path: ":opensearch-resource-sharing-spi") +} + +shadowJar { + archiveClassifier.set(null) } java { @@ -99,3 +104,5 @@ publishing { } } } + +compileJava.dependsOn(':opensearch-security-common:shadowJar') diff --git a/common/build.gradle b/common/build.gradle index 2b8e67add5..c085490161 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -22,12 +22,16 @@ repositories { dependencies { compileOnly "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" - implementation project(path: ":opensearch-resource-sharing-spi", configuration: 'shadow') + compileOnly project(path: ":opensearch-resource-sharing-spi") compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" compileOnly 'com.password4j:password4j:1.8.2' compileOnly "com.google.guava:guava:${guava_version}" } +shadowJar { + archiveClassifier.set(null) +} + java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 @@ -89,3 +93,5 @@ publishing { } } } + +publishShadowPublicationToMavenLocal.mustRunAfter shadowJar From 2df199b8819e432dd948affcfdb16990b16f7b51 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 19 Mar 2025 16:26:59 -0400 Subject: [PATCH 5/7] Disables jar for common Signed-off-by: Darshit Chanpura --- common/build.gradle | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/common/build.gradle b/common/build.gradle index c085490161..04588ced17 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -32,6 +32,11 @@ shadowJar { archiveClassifier.set(null) } +jar { + enabled false +} + + java { sourceCompatibility = JavaVersion.VERSION_21 targetCompatibility = JavaVersion.VERSION_21 From f18a5302718fd5e32e8d9e567f94cee723656892 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Wed, 19 Mar 2025 18:53:48 -0400 Subject: [PATCH 6/7] Removes common package and refactors classes Signed-off-by: Darshit Chanpura --- .github/workflows/ci.yml | 15 +- build.gradle | 25 +- client/build.gradle | 5 +- .../resources/ResourceSharingNodeClient.java | 8 +- common/build.gradle | 102 ---- .../security/common/DefaultObjectMapper.java | 298 ---------- .../common/auditlog/impl/AuditCategory.java | 40 -- .../common/configuration/AdminDNs.java | 162 ----- .../common/dlic/rest/api/Responses.java | 106 ---- .../security/common/package-info.java | 15 - .../common/support/ConfigConstants.java | 404 ------------- .../security/common/support/Utils.java | 285 --------- .../common/support/WildcardMatcher.java | 556 ------------------ .../security/common/user/AuthCredentials.java | 254 -------- .../common/user/CustomAttributesAware.java | 34 -- .../opensearch/security/common/user/User.java | 312 ---------- scripts/build.sh | 1 - settings.gradle | 3 - .../framework/cluster/TestRestClient.java | 15 +- .../security/OpenSearchSecurityPlugin.java | 26 +- .../security/auth/BackendRegistry.java | 22 +- .../security}/auth/UserSubjectImpl.java | 6 +- .../security/dlic/rest/support/Utils.java | 6 +- .../resources/ResourceAccessHandler.java | 17 +- .../resources/ResourceIndexListener.java | 8 +- .../resources/ResourcePluginInfo.java | 10 +- .../security}/resources/ResourceProvider.java | 5 +- .../resources/ResourceSharingConstants.java | 2 +- .../ResourceSharingIndexHandler.java | 4 +- ...ourceSharingIndexManagementRepository.java | 2 +- .../resources/rest/ResourceAccessAction.java | 2 +- .../resources/rest/ResourceAccessRequest.java | 2 +- .../rest/ResourceAccessRequestParams.java | 2 +- .../rest/ResourceAccessResponse.java | 2 +- .../rest/ResourceAccessRestAction.java | 22 +- .../rest/ResourceAccessTransportAction.java | 4 +- .../security/support/ConfigConstants.java | 7 +- .../security/support/ConfigHelper.java | 1 - .../support/SecurityIndexHandler.java | 1 - .../security/support/SecurityJsonNode.java | 2 - .../security/support/WildcardMatcher.java | 2 +- .../security/support/YamlConfigReader.java | 1 - .../org/opensearch/security/user/User.java | 12 +- .../security/auth/UserSubjectImplTests.java | 3 +- .../security/support/ConfigReaderTest.java | 1 - .../support/SecurityIndexHandlerTest.java | 1 - 46 files changed, 116 insertions(+), 2697 deletions(-) delete mode 100644 common/build.gradle delete mode 100644 common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java delete mode 100644 common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java delete mode 100644 common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java delete mode 100644 common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java delete mode 100644 common/src/main/java/org/opensearch/security/common/package-info.java delete mode 100644 common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java delete mode 100644 common/src/main/java/org/opensearch/security/common/support/Utils.java delete mode 100644 common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java delete mode 100644 common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java delete mode 100644 common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java delete mode 100644 common/src/main/java/org/opensearch/security/common/user/User.java rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/auth/UserSubjectImpl.java (90%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/ResourceAccessHandler.java (97%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/ResourceIndexListener.java (95%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/ResourcePluginInfo.java (87%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/ResourceProvider.java (75%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/ResourceSharingConstants.java (92%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/ResourceSharingIndexHandler.java (99%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/ResourceSharingIndexManagementRepository.java (97%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/rest/ResourceAccessAction.java (93%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/rest/ResourceAccessRequest.java (99%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/rest/ResourceAccessRequestParams.java (93%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/rest/ResourceAccessResponse.java (98%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/rest/ResourceAccessRestAction.java (83%) rename {common/src/main/java/org/opensearch/security/common => src/main/java/org/opensearch/security}/resources/rest/ResourceAccessTransportAction.java (97%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ed86681fe..cb48bd4b37 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,7 +48,6 @@ jobs: run: | ./gradlew clean \ :opensearch-resource-sharing-spi:publishToMavenLocal \ - :opensearch-security-common:publishToMavenLocal \ :opensearch-security-client:publishToMavenLocal \ -Dbuild.snapshot=false @@ -337,12 +336,6 @@ jobs: ./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 - # Publish Common - ./gradlew clean :opensearch-security-common:publishToMavenLocal && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar - ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot-all.jar - ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.snapshot=false -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-all.jar - ./gradlew clean :opensearch-security-common:publishToMavenLocal -Dbuild.version_qualifier=$test_qualifier && test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT-all.jar - # Publish Client ./gradlew clean :opensearch-security-client:publishToMavenLocal && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version-all.jar ./gradlew clean :opensearch-security-client:publishToMavenLocal -Dbuild.snapshot=false && test -s ./client/build/libs/opensearch-security-client-$security_plugin_version_no_snapshot-all.jar @@ -353,37 +346,31 @@ jobs: ./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 && \ - test -s ./common/build/libs/opensearch-security-common-$security_plugin_version.jar && \ test -s ./client/build/libs/opensearch-security-client-$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 && \ - test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_no_snapshot.jar && \ test -s ./client/build/libs/opensearch-security-client-$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 && \ - test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier.jar && \ test -s ./client/build/libs/opensearch-security-client-$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 && \ - test -s ./common/build/libs/opensearch-security-common-$security_plugin_version_only_number-$test_qualifier-SNAPSHOT.jar && \ test -s ./client/build/libs/opensearch-security-client-$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 && \ - test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar + 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 && \ - test -s ./common/build/libs/opensearch-security-common-$security_plugin_version-all.jar && \ test -s ./client/build/libs/opensearch-security-client-$security_plugin_version-all.jar - name: List files in build directory on failure diff --git a/build.gradle b/build.gradle index 0b52259149..4a091bd69b 100644 --- a/build.gradle +++ b/build.gradle @@ -442,14 +442,6 @@ publishing { } } -repositories { - mavenLocal() - mavenCentral() - maven { url "https://plugins.gradle.org/m2/" } - maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } - maven { url "https://artifacts.opensearch.org/snapshots/lucene/" } - maven { url "https://build.shibboleth.net/nexus/content/repositories/releases" } -} tasks.test.finalizedBy(jacocoTestReport) // report is always generated after tests run @@ -514,6 +506,15 @@ configurations { } allprojects { + repositories { + mavenLocal() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } + maven { url "https://artifacts.opensearch.org/snapshots/lucene/" } + maven { url "https://build.shibboleth.net/nexus/content/repositories/releases" } + } + configurations { integrationTestImplementation.extendsFrom implementation compile.extendsFrom compileOnly @@ -569,7 +570,6 @@ allprojects { 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') - integrationTestImplementation project(path: ":${rootProject.name}-common", configuration: 'shadow') integrationTestImplementation project(path: ":${rootProject.name}-client", configuration: 'shadow') } } @@ -594,6 +594,9 @@ sourceSets { //add new task that runs integration tests task integrationTest(type: Test) { + filter { + excludeTestsMatching 'org.opensearch.sample.*ResourcePlugin*' + } doFirst { // Only run resources tests on resource-test CI environments or locally if (System.getenv('CI_ENVIRONMENT') != 'resource-test' && System.getenv('CI_ENVIRONMENT') != null) { @@ -642,7 +645,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { - implementation project(path: ":${rootProject.name}-common", configuration: 'shadow') + 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}" @@ -802,8 +805,6 @@ dependencies { implementation('com.google.googlejavaformat:google-java-format:1.25.2') { exclude group: 'com.google.guava' } - - testImplementation project(path: ":${rootProject.name}-common", configuration: 'shadow') } jar { diff --git a/client/build.gradle b/client/build.gradle index 9923979f24..e1876dac24 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -34,9 +34,8 @@ repositories { dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" - // SPI dependency comes through common - compileOnly project(path: ":${rootProject.name}-common", configuration: 'shadow') compileOnly project(path: ":opensearch-resource-sharing-spi") + compileOnly project(":") } shadowJar { @@ -104,5 +103,3 @@ publishing { } } } - -compileJava.dependsOn(':opensearch-security-common:shadowJar') diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index cbdbabc681..12550284f8 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -17,13 +17,13 @@ import org.opensearch.common.settings.Settings; import org.opensearch.core.action.ActionListener; import org.opensearch.core.rest.RestStatus; -import org.opensearch.security.common.resources.rest.ResourceAccessAction; -import org.opensearch.security.common.resources.rest.ResourceAccessRequest; -import org.opensearch.security.common.resources.rest.ResourceAccessResponse; -import org.opensearch.security.common.support.ConfigConstants; +import org.opensearch.security.resources.rest.ResourceAccessAction; +import org.opensearch.security.resources.rest.ResourceAccessRequest; +import org.opensearch.security.resources.rest.ResourceAccessResponse; import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.security.support.ConfigConstants; import org.opensearch.transport.client.Client; /** diff --git a/common/build.gradle b/common/build.gradle deleted file mode 100644 index 04588ced17..0000000000 --- a/common/build.gradle +++ /dev/null @@ -1,102 +0,0 @@ -/* - * 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 "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" - compileOnly "org.opensearch.plugin:lang-painless:${opensearch_version}" - compileOnly project(path: ":opensearch-resource-sharing-spi") - compileOnly "org.apache.commons:commons-lang3:${versions.commonslang}" - compileOnly 'com.password4j:password4j:1.8.2' - compileOnly "com.google.guava:guava:${guava_version}" -} - -shadowJar { - archiveClassifier.set(null) -} - -jar { - enabled false -} - - -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 Security Common") - packaging = "jar" - description.set("OpenSearch Security Common") - 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" - } - } -} - -publishShadowPublicationToMavenLocal.mustRunAfter shadowJar diff --git a/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java b/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java deleted file mode 100644 index 7a2dc137a6..0000000000 --- a/common/src/main/java/org/opensearch/security/common/DefaultObjectMapper.java +++ /dev/null @@ -1,298 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common; - -import java.io.IOException; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.Map; -import java.util.Set; - -import com.google.common.collect.ImmutableSet; -import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.core.JsonGenerator; -import com.fasterxml.jackson.core.JsonParser; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.InjectableValues; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializerProvider; -import com.fasterxml.jackson.databind.exc.InvalidFormatException; -import com.fasterxml.jackson.databind.exc.MismatchedInputException; -import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition; -import com.fasterxml.jackson.databind.module.SimpleModule; -import com.fasterxml.jackson.databind.ser.std.StdSerializer; -import com.fasterxml.jackson.databind.type.TypeFactory; -import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; - -import org.opensearch.SpecialPermission; - -class ConfigMapSerializer extends StdSerializer> { - private static final Set SENSITIVE_CONFIG_KEYS = Set.of("password"); - - @SuppressWarnings("unchecked") - public ConfigMapSerializer() { - // Pass Map.class to the superclass - super((Class>) (Class) Map.class); - } - - @Override - public void serialize(Map value, JsonGenerator gen, SerializerProvider serializers) throws IOException { - gen.writeStartObject(); - for (Map.Entry entry : value.entrySet()) { - if (SENSITIVE_CONFIG_KEYS.contains(entry.getKey())) { - gen.writeStringField(entry.getKey(), "******"); // Redact - } else { - gen.writeObjectField(entry.getKey(), entry.getValue()); - } - } - gen.writeEndObject(); - } -} - -public class DefaultObjectMapper { - public static final ObjectMapper objectMapper = new ObjectMapper(); - public final static ObjectMapper YAML_MAPPER = new ObjectMapper(new YAMLFactory()); - private static final ObjectMapper defaulOmittingObjectMapper = new ObjectMapper(); - - static { - objectMapper.setSerializationInclusion(Include.NON_NULL); - // exclude sensitive information from the request body, - // if jackson cant parse the entity, e.g. passwords, hashes and so on, - // but provides which property is unknown - objectMapper.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); - defaulOmittingObjectMapper.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); - YAML_MAPPER.disable(JsonParser.Feature.INCLUDE_SOURCE_IN_LOCATION); - // objectMapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); - objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); - defaulOmittingObjectMapper.setSerializationInclusion(Include.NON_DEFAULT); - defaulOmittingObjectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); - YAML_MAPPER.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); - } - - private DefaultObjectMapper() {} - - public static void inject(final InjectableValues.Std injectableValues) { - objectMapper.setInjectableValues(injectableValues); - YAML_MAPPER.setInjectableValues(injectableValues); - defaulOmittingObjectMapper.setInjectableValues(injectableValues); - } - - public static boolean getOrDefault(Map properties, String key, boolean defaultValue) throws JsonProcessingException { - Object value = properties.get(key); - if (value == null) { - return defaultValue; - } else if (value instanceof Boolean) { - return (boolean) value; - } else if (value instanceof String) { - String text = ((String) value).trim(); - if ("true".equals(text) || "True".equals(text)) { - return true; - } - if ("false".equals(text) || "False".equals(text)) { - return false; - } - throw InvalidFormatException.from( - null, - "Cannot deserialize value of type 'boolean' from String \"" + text + "\": only \"true\" or \"false\" recognized)", - null, - Boolean.class - ); - } - throw MismatchedInputException.from( - null, - Boolean.class, - "Cannot deserialize instance of 'boolean' out of '" + value + "' (Property: " + key + ")" - ); - } - - @SuppressWarnings("unchecked") - public static T getOrDefault(Map properties, String key, T defaultValue) { - T value = (T) properties.get(key); - return value != null ? value : defaultValue; - } - - @SuppressWarnings("removal") - public static T readTree(JsonNode node, Class clazz) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.treeToValue(node, clazz)); - } catch (final PrivilegedActionException e) { - throw (IOException) e.getCause(); - } - } - - @SuppressWarnings("removal") - public static T readValue(String string, Class clazz) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, clazz)); - } catch (final PrivilegedActionException e) { - throw (IOException) e.getCause(); - } - } - - @SuppressWarnings("removal") - public static JsonNode readTree(String string) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readTree(string)); - } catch (final PrivilegedActionException e) { - throw (IOException) e.getCause(); - } - } - - @SuppressWarnings("removal") - public static String writeValueAsString(Object value, boolean omitDefaults) throws JsonProcessingException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged( - (PrivilegedExceptionAction) () -> (omitDefaults ? defaulOmittingObjectMapper : objectMapper).writeValueAsString( - value - ) - ); - } catch (final PrivilegedActionException e) { - throw (JsonProcessingException) e.getCause(); - } - - } - - @SuppressWarnings("removal") - public static String writeValueAsStringAndRedactSensitive(Object value) throws JsonProcessingException { - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - SimpleModule module = new SimpleModule(); - module.addSerializer(new ConfigMapSerializer()); - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(module); - - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> mapper.writeValueAsString(value)); - } catch (final PrivilegedActionException e) { - throw (JsonProcessingException) e.getCause(); - } - - } - - @SuppressWarnings("removal") - public static T readValue(String string, TypeReference tr) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged(new PrivilegedExceptionAction() { - @Override - public T run() throws Exception { - return objectMapper.readValue(string, tr); - } - }); - } catch (final PrivilegedActionException e) { - throw (IOException) e.getCause(); - } - - } - - @SuppressWarnings("removal") - public static T readValue(String string, JavaType jt) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.readValue(string, jt)); - } catch (final PrivilegedActionException e) { - throw (IOException) e.getCause(); - } - } - - @SuppressWarnings("removal") - public static T convertValue(JsonNode jsonNode, JavaType jt) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> objectMapper.convertValue(jsonNode, jt)); - } catch (final PrivilegedActionException e) { - throw (IOException) e.getCause(); - } - } - - public static TypeFactory getTypeFactory() { - return objectMapper.getTypeFactory(); - } - - public static Set getFields(Class cls) { - return objectMapper.getSerializationConfig() - .introspect(getTypeFactory().constructType(cls)) - .findProperties() - .stream() - .map(BeanPropertyDefinition::getName) - .collect(ImmutableSet.toImmutableSet()); - } -} diff --git a/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java b/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java deleted file mode 100644 index 3526404bbd..0000000000 --- a/common/src/main/java/org/opensearch/security/common/auditlog/impl/AuditCategory.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.auditlog.impl; - -import java.util.Collection; -import java.util.Collections; -import java.util.Set; - -import com.google.common.collect.ImmutableSet; - -public enum AuditCategory { - BAD_HEADERS, - FAILED_LOGIN, - MISSING_PRIVILEGES, - GRANTED_PRIVILEGES, - OPENDISTRO_SECURITY_INDEX_ATTEMPT, - SSL_EXCEPTION, - AUTHENTICATED, - INDEX_EVENT, - COMPLIANCE_DOC_READ, - COMPLIANCE_DOC_WRITE, - COMPLIANCE_EXTERNAL_CONFIG, - COMPLIANCE_INTERNAL_CONFIG_READ, - COMPLIANCE_INTERNAL_CONFIG_WRITE; - - public static Set parse(final Collection categories) { - if (categories.isEmpty()) return Collections.emptySet(); - - return categories.stream().map(String::toUpperCase).map(AuditCategory::valueOf).collect(ImmutableSet.toImmutableSet()); - } -} diff --git a/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java b/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java deleted file mode 100644 index 22647e6685..0000000000 --- a/common/src/main/java/org/opensearch/security/common/configuration/AdminDNs.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.configuration; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import java.util.function.Function; -import javax.naming.InvalidNameException; -import javax.naming.ldap.LdapName; - -import com.google.common.collect.ImmutableMap; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.common.support.ConfigConstants; -import org.opensearch.security.common.support.WildcardMatcher; -import org.opensearch.security.common.user.User; - -public class AdminDNs { - - protected final Logger log = LogManager.getLogger(AdminDNs.class); - private final Set adminDn = new HashSet(); - private final Set adminUsernames = new HashSet(); - private final Map allowedDnsImpersonations; - private final Map allowedRestImpersonations; - private boolean injectUserEnabled; - private boolean injectAdminUserEnabled; - - public AdminDNs(final Settings settings) { - - this.injectUserEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false); - this.injectAdminUserEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, false); - - final List adminDnsA = settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList()); - - for (String dn : adminDnsA) { - try { - log.debug("{} is registered as an admin dn", dn); - adminDn.add(new LdapName(dn)); - } catch (final InvalidNameException e) { - // make sure to log correctly depending on user injection settings - if (injectUserEnabled && injectAdminUserEnabled) { - if (log.isDebugEnabled()) { - log.debug("Admin DN not an LDAP name, but admin user injection enabled. Will add {} to admin usernames", dn); - } - adminUsernames.add(dn); - } else { - log.error("Unable to parse admin dn {}", dn, e); - } - } - } - - log.debug("Loaded {} admin DN's {}", adminDn.size(), adminDn); - - final Settings impersonationDns = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + "."); - - allowedDnsImpersonations = impersonationDns.keySet() - .stream() - .map(this::toLdapName) - .filter(Objects::nonNull) - .collect( - ImmutableMap.toImmutableMap( - Function.identity(), - ldapName -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + "." + ldapName)) - ) - ); - - log.debug("Loaded {} impersonation DN's {}", allowedDnsImpersonations.size(), allowedDnsImpersonations); - - final Settings impersonationUsersRest = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "."); - - allowedRestImpersonations = impersonationUsersRest.keySet() - .stream() - .collect( - ImmutableMap.toImmutableMap( - Function.identity(), - user -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + user)) - ) - ); - - log.debug("Loaded {} impersonation users for REST {}", allowedRestImpersonations.size(), allowedRestImpersonations); - } - - private LdapName toLdapName(String dn) { - try { - return new LdapName(dn); - } catch (final InvalidNameException e) { - log.error("Unable to parse allowedImpersonations dn {}", dn, e); - } - return null; - } - - public boolean isAdmin(User user) { - if (isAdminDN(user.getName())) { - return true; - } - - // ThreadContext injected user, may be admin user, only if both flags are enabled and user is injected - if (injectUserEnabled && injectAdminUserEnabled && user.isInjected() && adminUsernames.contains(user.getName())) { - return true; - } - return false; - } - - public boolean isAdminDN(String dn) { - - if (dn == null) return false; - - try { - return isAdminDN(new LdapName(dn)); - } catch (InvalidNameException e) { - return false; - } - } - - private boolean isAdminDN(LdapName dn) { - if (dn == null) return false; - - boolean isAdmin = adminDn.contains(dn); - - if (log.isTraceEnabled()) { - log.trace("Is principal {} an admin cert? {}", dn.toString(), isAdmin); - } - - return isAdmin; - } - - public boolean isRestImpersonationAllowed(final String originalUser, final String impersonated) { - return (originalUser != null) - ? allowedRestImpersonations.getOrDefault(originalUser, WildcardMatcher.NONE).test(impersonated) - : false; - } -} diff --git a/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java b/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java deleted file mode 100644 index e2258e9e6e..0000000000 --- a/common/src/main/java/org/opensearch/security/common/dlic/rest/api/Responses.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.dlic.rest.api; - -import java.io.IOException; - -import org.opensearch.ExceptionsHelper; -import org.opensearch.core.rest.RestStatus; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.rest.BytesRestResponse; -import org.opensearch.rest.RestChannel; -import org.opensearch.rest.RestRequest; - -public class Responses { - - public static void ok(final RestChannel channel, final String message) { - response(channel, RestStatus.OK, message); - } - - public static void ok(final RestChannel channel, final ToXContent toXContent) { - response(channel, RestStatus.OK, toXContent); - } - - public static void created(final RestChannel channel, final String message) { - response(channel, RestStatus.CREATED, message); - } - - public static void methodNotImplemented(final RestChannel channel, final RestRequest.Method method) { - notImplemented(channel, "Method " + method.name() + " not supported for this action."); - } - - public static void notImplemented(final RestChannel channel, final String message) { - response(channel, RestStatus.NOT_IMPLEMENTED, message); - } - - public static void notFound(final RestChannel channel, final String message) { - response(channel, RestStatus.NOT_FOUND, message); - } - - public static void conflict(final RestChannel channel, final String message) { - response(channel, RestStatus.CONFLICT, message); - } - - public static void internalServerError(final RestChannel channel, final String message) { - response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); - } - - public static void forbidden(final RestChannel channel, final String message) { - response(channel, RestStatus.FORBIDDEN, message); - } - - public static void badRequest(final RestChannel channel, final String message) { - response(channel, RestStatus.BAD_REQUEST, message); - } - - public static void unauthorized(final RestChannel channel) { - response(channel, RestStatus.UNAUTHORIZED, "Unauthorized"); - } - - public static void response(RestChannel channel, RestStatus status, String message) { - response(channel, status, payload(status, message)); - } - - public static void response(final RestChannel channel, final RestStatus status, final ToXContent toXContent) { - try (final var builder = channel.newBuilder()) { - toXContent.toXContent(builder, ToXContent.EMPTY_PARAMS); - channel.sendResponse(new BytesRestResponse(status, builder)); - } catch (final IOException ioe) { - throw ExceptionsHelper.convertToOpenSearchException(ioe); - } - } - - public static ToXContent forbiddenMessage(final String message) { - return payload(RestStatus.FORBIDDEN, message); - } - - public static ToXContent badRequestMessage(final String message) { - return payload(RestStatus.BAD_REQUEST, message); - } - - public static ToXContent methodNotImplementedMessage(final RestRequest.Method method) { - return payload(RestStatus.NOT_FOUND, "Method " + method.name() + " not supported for this action."); - } - - public static ToXContent notFoundMessage(final String message) { - return payload(RestStatus.NOT_FOUND, message); - } - - public static ToXContent conflictMessage(final String message) { - return payload(RestStatus.CONFLICT, message); - } - - public static ToXContent payload(final RestStatus status, final String message) { - return (builder, params) -> builder.startObject().field("status", status.name()).field("message", message).endObject(); - } - -} diff --git a/common/src/main/java/org/opensearch/security/common/package-info.java b/common/src/main/java/org/opensearch/security/common/package-info.java deleted file mode 100644 index 01e2ead134..0000000000 --- a/common/src/main/java/org/opensearch/security/common/package-info.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -/** - * This package defines common classes required to implement resource access control in OpenSearch. - * TODO: At present it contains multiple duplicates, which will be address in a fast follow PR. - * - * @opensearch.experimental - */ -package org.opensearch.security.common; diff --git a/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java b/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java deleted file mode 100644 index 2aafb9898a..0000000000 --- a/common/src/main/java/org/opensearch/security/common/support/ConfigConstants.java +++ /dev/null @@ -1,404 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.support; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; - -import org.opensearch.common.settings.Settings; -import org.opensearch.security.common.auditlog.impl.AuditCategory; - -import com.password4j.types.Hmac; - -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 OPENSEARCH_SECURITY_DISABLED = SECURITY_SETTINGS_PREFIX + "disabled"; - public static final boolean OPENSEARCH_SECURITY_DISABLED_DEFAULT = false; - - public static final String OPENDISTRO_SECURITY_CHANNEL_TYPE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "channel_type"; - - public static final String OPENDISTRO_SECURITY_ORIGIN = OPENDISTRO_SECURITY_CONFIG_PREFIX + "origin"; - public static final String OPENDISTRO_SECURITY_ORIGIN_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "origin_header"; - - public static final String OPENDISTRO_SECURITY_DLS_QUERY_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_query"; - - public static final String OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX - + "dls_filter_level_query"; - public static final String OPENDISTRO_SECURITY_DLS_FILTER_LEVEL_QUERY_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX - + "dls_filter_level_query_t"; - - public static final String OPENDISTRO_SECURITY_DLS_MODE_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_mode"; - public static final String OPENDISTRO_SECURITY_DLS_MODE_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_mode_t"; - - public static final String OPENDISTRO_SECURITY_FLS_FIELDS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "fls_fields"; - - public static final String OPENDISTRO_SECURITY_MASKED_FIELD_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "masked_fields"; - - public static final String OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "doc_allowlist"; - public static final String OPENDISTRO_SECURITY_DOC_ALLOWLIST_TRANSIENT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "doc_allowlist_t"; - - public static final String OPENDISTRO_SECURITY_FILTER_LEVEL_DLS_DONE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "filter_level_dls_done"; - - public static final String OPENDISTRO_SECURITY_DLS_QUERY_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "dls_query_ccs"; - - public static final String OPENDISTRO_SECURITY_FLS_FIELDS_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "fls_fields_ccs"; - - public static final String OPENDISTRO_SECURITY_MASKED_FIELD_CCS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "masked_fields_ccs"; - - public static final String OPENDISTRO_SECURITY_CONF_REQUEST_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "conf_request"; - - public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS = OPENDISTRO_SECURITY_CONFIG_PREFIX + "remote_address"; - public static final String OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "remote_address_header"; - - public static final String OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX - + "initial_action_class_header"; - - /** - * Set by SSL plugin for https requests only - */ - public static final String OPENDISTRO_SECURITY_SSL_PEER_CERTIFICATES = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_peer_certificates"; - - /** - * Set by SSL plugin for https requests only - */ - public static final String OPENDISTRO_SECURITY_SSL_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_principal"; - - /** - * If this is set to TRUE then the request comes from a Server Node (fully trust) - * Its expected that there is a _opendistro_security_user attached as header - */ - public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_INTERCLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX - + "ssl_transport_intercluster_request"; - - public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_TRUSTED_CLUSTER_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX - + "ssl_transport_trustedcluster_request"; - - // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions - public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_EXTENSION_REQUEST = OPENDISTRO_SECURITY_CONFIG_PREFIX - + "ssl_transport_extension_request"; - // CS-ENFORCE-SINGLE - - /** - * Set by the SSL plugin, this is the peer node certificate on the transport layer - */ - public static final String OPENDISTRO_SECURITY_SSL_TRANSPORT_PRINCIPAL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "ssl_transport_principal"; - - public static final String OPENDISTRO_SECURITY_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user"; - public static final String OPENDISTRO_SECURITY_USER_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_header"; - - // persistent header. This header is set once and cannot be stashed - public static final String OPENDISTRO_SECURITY_AUTHENTICATED_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "authenticated_user"; - - public static final String OPENDISTRO_SECURITY_INITIATING_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "_initiating_user"; - - public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; - - public static final String OPENDISTRO_SECURITY_INJECTED_USER = "injected_user"; - public static final String OPENDISTRO_SECURITY_INJECTED_USER_HEADER = "injected_user_header"; - - public static final String OPENDISTRO_SECURITY_XFF_DONE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "xff_done"; - - public static final String SSO_LOGOUT_URL = OPENDISTRO_SECURITY_CONFIG_PREFIX + "sso_logout_url"; - - public static final String OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX = ".opendistro_security"; - - 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 = 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" - // "indices:data/write/bulk" - )) - ); - - 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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"; - public static final String OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT = "opendistro_security.audit.enable_transport"; - public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES = - "opendistro_security.audit.config.disabled_transport_categories"; - public static final String OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES = - "opendistro_security.audit.config.disabled_rest_categories"; - public static final List OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT = ImmutableList.of( - AuditCategory.AUTHENTICATED.toString(), - AuditCategory.GRANTED_PRIVILEGES.toString() - ); - 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 = 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 = SECURITY_SETTINGS_PREFIX + "audit.config."; - - // Internal Opensearch data_stream - public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_NAME = "data_stream.name"; - public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_MANAGE = "data_stream.template.manage"; - public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NAME = "data_stream.template.name"; - public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NUMBER_OF_REPLICAS = "data_stream.template.number_of_replicas"; - public static final String SECURITY_AUDIT_OPENSEARCH_DATASTREAM_TEMPLATE_NUMBER_OF_SHARDS = "data_stream.template.number_of_shards"; - - // Internal / External OpenSearch - public static final String SECURITY_AUDIT_OPENSEARCH_INDEX = "index"; - public static final String SECURITY_AUDIT_OPENSEARCH_TYPE = "type"; - - // External OpenSearch - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS = "http_endpoints"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME = "username"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD = "password"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL = "enable_ssl"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES = "verify_hostnames"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH = "enable_ssl_client_auth"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH = "pemkey_filepath"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_CONTENT = "pemkey_content"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD = "pemkey_password"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH = "pemcert_filepath"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_CONTENT = "pemcert_content"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH = "pemtrustedcas_filepath"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT = "pemtrustedcas_content"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS = "cert_alias"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_CIPHERS = "enabled_ssl_ciphers"; - public static final String SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_PROTOCOLS = "enabled_ssl_protocols"; - - // Webhooks - public static final String SECURITY_AUDIT_WEBHOOK_URL = "webhook.url"; - public static final String SECURITY_AUDIT_WEBHOOK_FORMAT = "webhook.format"; - public static final String SECURITY_AUDIT_WEBHOOK_SSL_VERIFY = "webhook.ssl.verify"; - public static final String SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH = "webhook.ssl.pemtrustedcas_filepath"; - public static final String SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT = "webhook.ssl.pemtrustedcas_content"; - - // Log4j - public static final String SECURITY_AUDIT_LOG4J_LOGGER_NAME = "log4j.logger_name"; - public static final String SECURITY_AUDIT_LOG4J_LEVEL = "log4j.level"; - - // retry - 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 = 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 = 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 = SECURITY_SETTINGS_PREFIX - + "allow_default_init_securityindex.use_cluster_state"; - - 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 = SECURITY_SETTINGS_PREFIX + "roles_mapping_resolution"; - - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY = - "opendistro_security.compliance.history.write.metadata_only"; - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY = - "opendistro_security.compliance.history.read.metadata_only"; - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS = - "opendistro_security.compliance.history.read.watched_fields"; - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES = - "opendistro_security.compliance.history.write.watched_indices"; - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS = - "opendistro_security.compliance.history.write.log_diffs"; - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS = - "opendistro_security.compliance.history.read.ignore_users"; - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS = - "opendistro_security.compliance.history.write.ignore_users"; - public static final String OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED = - "opendistro_security.compliance.history.external_config_enabled"; - public static final String OPENDISTRO_SECURITY_SOURCE_FIELD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "source_field_context"; - public static final String SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION = 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 = 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 = 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, - BACKENDROLES_ONLY, - BOTH - } - - 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 = 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 = 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 = 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 = SECURITY_SETTINGS_PREFIX + "protected_indices.roles"; - public static final List SECURITY_PROTECTED_INDICES_ROLES_DEFAULT = Collections.emptyList(); - - // Roles injection for plugins - public static final String OPENDISTRO_SECURITY_INJECTED_ROLES = "opendistro_security_injected_roles"; - public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_HEADER = "opendistro_security_injected_roles_header"; - - // Roles validation for the plugins - public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION = "opendistro_security_injected_roles_validation"; - public static final String OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION_HEADER = - "opendistro_security_injected_roles_validation_header"; - - // System indices settings - public static final String SYSTEM_INDEX_PERMISSION = "system:admin/system_index"; - 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 = 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 = 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 = 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 = SECURITY_SETTINGS_PREFIX + "use_jdk_serialization"; - - // On-behalf-of endpoints settings - // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings - public static final String EXTENSIONS_BWC_PLUGIN_MODE = "bwcPluginMode"; - public static final boolean EXTENSIONS_BWC_PLUGIN_MODE_DEFAULT = false; - // CS-ENFORCE-SINGLE - - // Variable for initial admin password support - public static final String OPENSEARCH_INITIAL_ADMIN_PASSWORD = "OPENSEARCH_INITIAL_ADMIN_PASSWORD"; - - // Resource sharing feature-flag - public static final String OPENSEARCH_RESOURCE_SHARING_ENABLED = SECURITY_SETTINGS_PREFIX + "resource_sharing.enabled"; - public static final boolean OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT = true; - - public static Set getSettingAsSet( - final Settings settings, - final String key, - final List defaultList, - final boolean ignoreCaseForNone - ) { - final List list = settings.getAsList(key, defaultList); - if (list.size() == 1 && "NONE".equals(ignoreCaseForNone ? list.get(0).toUpperCase() : list.get(0))) { - return Collections.emptySet(); - } - return ImmutableSet.copyOf(list); - } -} diff --git a/common/src/main/java/org/opensearch/security/common/support/Utils.java b/common/src/main/java/org/opensearch/security/common/support/Utils.java deleted file mode 100644 index ffdc8d9390..0000000000 --- a/common/src/main/java/org/opensearch/security/common/support/Utils.java +++ /dev/null @@ -1,285 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.support; - -import java.io.IOException; -import java.io.UncheckedIOException; -import java.security.AccessController; -import java.security.PrivilegedActionException; -import java.security.PrivilegedExceptionAction; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.apache.commons.lang3.tuple.Pair; - -import org.opensearch.ExceptionsHelper; -import org.opensearch.OpenSearchParseException; -import org.opensearch.SpecialPermission; -import org.opensearch.common.CheckedSupplier; -import org.opensearch.common.util.concurrent.ThreadContext; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; -import org.opensearch.common.xcontent.json.JsonXContent; -import org.opensearch.core.common.Strings; -import org.opensearch.core.common.bytes.BytesReference; -import org.opensearch.core.common.transport.TransportAddress; -import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.core.xcontent.ToXContent; -import org.opensearch.core.xcontent.XContentParser; -import org.opensearch.rest.NamedRoute; -import org.opensearch.rest.RestHandler.DeprecatedRoute; -import org.opensearch.rest.RestHandler.Route; -import org.opensearch.security.common.DefaultObjectMapper; -import org.opensearch.security.common.user.User; - -import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; - -public class Utils { - @Deprecated - public static final String LEGACY_OPENDISTRO_PREFIX = "_opendistro/_security"; - public static final String PLUGINS_PREFIX = "_plugins/_security"; - - public final static String PLUGIN_ROUTE_PREFIX = "/" + PLUGINS_PREFIX; - - @Deprecated - public final static String LEGACY_PLUGIN_ROUTE_PREFIX = "/" + LEGACY_OPENDISTRO_PREFIX; - - public final static String PLUGIN_API_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/api"; - - @Deprecated - public final static String LEGACY_PLUGIN_API_ROUTE_PREFIX = LEGACY_PLUGIN_ROUTE_PREFIX + "/api"; - - public final static String OPENDISTRO_API_DEPRECATION_MESSAGE = - "[_opendistro/_security] is a deprecated endpoint path. Please use _plugins/_security instead."; - - public final static String PLUGIN_RESOURCE_ROUTE_PREFIX = PLUGIN_ROUTE_PREFIX + "/resources"; - - private static final ObjectMapper internalMapper = new ObjectMapper(); - - public static Map convertJsonToxToStructuredMap(ToXContent jsonContent) { - Map map = null; - try { - final BytesReference bytes = XContentHelper.toXContent(jsonContent, XContentType.JSON, false); - map = XContentHelper.convertToMap(bytes, false, XContentType.JSON).v2(); - } catch (IOException e1) { - throw ExceptionsHelper.convertToOpenSearchException(e1); - } - - return map; - } - - public static Map convertJsonToxToStructuredMap(String jsonContent) { - try ( - XContentParser parser = XContentType.JSON.xContent() - .createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, jsonContent) - ) { - return parser.map(); - } catch (IOException e1) { - throw ExceptionsHelper.convertToOpenSearchException(e1); - } - } - - private static BytesReference convertStructuredMapToBytes(Map structuredMap) { - try { - return BytesReference.bytes(JsonXContent.contentBuilder().map(structuredMap)); - } catch (IOException e) { - throw new OpenSearchParseException("Failed to convert map", e); - } - } - - public static String convertStructuredMapToJson(Map structuredMap) { - try { - return XContentHelper.convertToJson(convertStructuredMapToBytes(structuredMap), false, XContentType.JSON); - } catch (IOException e) { - throw new OpenSearchParseException("Failed to convert map", e); - } - } - - public static JsonNode convertJsonToJackson(BytesReference jsonContent) { - try { - return DefaultObjectMapper.readTree(jsonContent.utf8ToString()); - } catch (IOException e1) { - throw ExceptionsHelper.convertToOpenSearchException(e1); - } - - } - - public static JsonNode toJsonNode(final String content) throws IOException { - return DefaultObjectMapper.readTree(content); - } - - public static Object toConfigObject(final JsonNode content, final Class clazz) throws IOException { - return DefaultObjectMapper.readTree(content, clazz); - } - - public static JsonNode convertJsonToJackson(ToXContent jsonContent, boolean omitDefaults) { - try { - return DefaultObjectMapper.readTree( - Strings.toString( - XContentType.JSON, - jsonContent, - new ToXContent.MapParams(Map.of("omit_defaults", String.valueOf(omitDefaults))) - ) - ); - } catch (IOException e1) { - throw ExceptionsHelper.convertToOpenSearchException(e1); - } - - } - - @SuppressWarnings("removal") - public static byte[] jsonMapToByteArray(Map jsonAsMap) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged((PrivilegedExceptionAction) () -> internalMapper.writeValueAsBytes(jsonAsMap)); - } catch (final PrivilegedActionException e) { - if (e.getCause() instanceof JsonProcessingException) { - throw (JsonProcessingException) e.getCause(); - } else if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } else { - throw new RuntimeException(e.getCause()); - } - } - } - - @SuppressWarnings("removal") - public static Map byteArrayToMutableJsonMap(byte[] jsonBytes) throws IOException { - - final SecurityManager sm = System.getSecurityManager(); - - if (sm != null) { - sm.checkPermission(new SpecialPermission()); - } - - try { - return AccessController.doPrivileged( - (PrivilegedExceptionAction>) () -> internalMapper.readValue( - jsonBytes, - new TypeReference>() { - } - ) - ); - } catch (final PrivilegedActionException e) { - if (e.getCause() instanceof IOException) { - throw (IOException) e.getCause(); - } else if (e.getCause() instanceof RuntimeException) { - throw (RuntimeException) e.getCause(); - } else { - throw new RuntimeException(e.getCause()); - } - } - } - - /** - * Generate field resource paths - * @param fields fields - * @param prefix prefix path - * @return new set of fields resource paths - */ - public static Set generateFieldResourcePaths(final Set fields, final String prefix) { - return fields.stream().map(field -> prefix + field).collect(ImmutableSet.toImmutableSet()); - } - - /** - * Add prefixes(_plugins/_security/api) to rest API routes - * @param routes routes - * @return new list of API routes prefixed with and _plugins/_security/api - */ - public static List addRoutesPrefix(List routes) { - return addRoutesPrefix(routes, PLUGIN_API_ROUTE_PREFIX); - } - - /** - * Add prefixes(_opendistro/_security/api) to rest API routes - * Deprecated in favor of addRoutesPrefix(List routes) - * @param routes routes - * @return new list of API routes prefixed with and _opendistro/_security/api - */ - @Deprecated - public static List addLegacyRoutesPrefix(List routes) { - return addDeprecatedRoutesPrefix(routes, LEGACY_PLUGIN_API_ROUTE_PREFIX); - } - - /** - * Add customized prefix(_opendistro... and _plugins...)to API rest routes - * @param routes routes - * @param prefixes all api prefix - * @return new list of API routes prefixed with the strings listed in prefixes - * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in - */ - public static List addRoutesPrefix(List routes, final String... prefixes) { - return routes.stream().flatMap(r -> Arrays.stream(prefixes).map(p -> { - if (r instanceof NamedRoute) { - NamedRoute nr = (NamedRoute) r; - return new NamedRoute.Builder().method(nr.getMethod()) - .path(p + nr.getPath()) - .uniqueName(nr.name()) - .legacyActionNames(nr.actionNames()) - .build(); - } - return new Route(r.getMethod(), p + r.getPath()); - })).collect(ImmutableList.toImmutableList()); - } - - /** - * Add prefixes(_plugins...) to rest API routes - * @param deprecatedRoutes Routes being deprecated - * @return new list of API routes prefixed with _opendistro... and _plugins... - *Total number of routes is expanded as twice as the number of routes passed in - */ - public static List addDeprecatedRoutesPrefix(List deprecatedRoutes) { - return addDeprecatedRoutesPrefix(deprecatedRoutes, LEGACY_PLUGIN_API_ROUTE_PREFIX, PLUGIN_API_ROUTE_PREFIX); - } - - /** - * Add customized prefix(_opendistro... and _plugins...)to API rest routes - * @param deprecatedRoutes Routes being deprecated - * @param prefixes all api prefix - * @return new list of API routes prefixed with the strings listed in prefixes - * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in - */ - public static List addDeprecatedRoutesPrefix(List deprecatedRoutes, final String... prefixes) { - return deprecatedRoutes.stream() - .flatMap(r -> Arrays.stream(prefixes).map(p -> new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) - .collect(ImmutableList.toImmutableList()); - } - - public static Pair userAndRemoteAddressFrom(final ThreadContext threadContext) { - final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final TransportAddress remoteAddress = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - return Pair.of(user, remoteAddress); - } - - public static T withIOException(final CheckedSupplier action) { - try { - return action.get(); - } catch (final IOException ioe) { - throw new UncheckedIOException(ioe); - } - } - -} diff --git a/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java b/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java deleted file mode 100644 index 4e5ab5b29b..0000000000 --- a/common/src/main/java/org/opensearch/security/common/support/WildcardMatcher.java +++ /dev/null @@ -1,556 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.support; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.regex.Pattern; -import java.util.stream.Collector; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import com.google.common.base.Preconditions; -import com.google.common.collect.ImmutableSet; - -public abstract class WildcardMatcher implements Predicate { - - public static final WildcardMatcher ANY = new WildcardMatcher() { - - @Override - public boolean matchAny(Stream candidates) { - return true; - } - - @Override - public boolean matchAny(Collection candidates) { - return true; - } - - @Override - public boolean matchAny(String... candidates) { - return true; - } - - @Override - public boolean matchAll(Stream candidates) { - return true; - } - - @Override - public boolean matchAll(Collection candidates) { - return true; - } - - @Override - public boolean matchAll(String[] candidates) { - return true; - } - - @Override - public > T getMatchAny(Stream candidates, Collector collector) { - return candidates.collect(collector); - } - - @Override - public boolean test(String candidate) { - return true; - } - - @Override - public String toString() { - return "*"; - } - }; - - public static final WildcardMatcher NONE = new WildcardMatcher() { - - @Override - public boolean matchAny(Stream candidates) { - return false; - } - - @Override - public boolean matchAny(Collection candidates) { - return false; - } - - @Override - public boolean matchAny(String... candidates) { - return false; - } - - @Override - public boolean matchAll(Stream candidates) { - return false; - } - - @Override - public boolean matchAll(Collection candidates) { - return false; - } - - @Override - public boolean matchAll(String[] candidates) { - return false; - } - - @Override - public > T getMatchAny(Stream candidates, Collector collector) { - return Stream.empty().collect(collector); - } - - @Override - public > T getMatchAny(Collection candidate, Collector collector) { - return Stream.empty().collect(collector); - } - - @Override - public > T getMatchAny(String[] candidate, Collector collector) { - return Stream.empty().collect(collector); - } - - @Override - public boolean test(String candidate) { - return false; - } - - @Override - public String toString() { - return ""; - } - }; - - public static WildcardMatcher from(String pattern, boolean caseSensitive) { - if (pattern == null) { - return NONE; - } else if (pattern.equals("*")) { - return ANY; - } else if (pattern.startsWith("/") && pattern.endsWith("/")) { - return new RegexMatcher(pattern, caseSensitive); - } else if (pattern.indexOf('?') >= 0 || pattern.indexOf('*') >= 0) { - return caseSensitive ? new SimpleMatcher(pattern) : new CasefoldingMatcher(pattern, SimpleMatcher::new); - } else { - return caseSensitive ? new Exact(pattern) : new CasefoldingMatcher(pattern, Exact::new); - } - } - - public static WildcardMatcher from(String pattern) { - return from(pattern, true); - } - - // This may in future use more optimized techniques to combine multiple WildcardMatchers in a single automaton - public static WildcardMatcher from(Stream stream, boolean caseSensitive) { - Collection matchers = stream.map(t -> { - if (t == null) { - return NONE; - } else if (t instanceof String) { - return WildcardMatcher.from(((String) t), caseSensitive); - } else if (t instanceof WildcardMatcher) { - return ((WildcardMatcher) t); - } - throw new UnsupportedOperationException("WildcardMatcher can't be constructed from " + t.getClass().getSimpleName()); - }).collect(ImmutableSet.toImmutableSet()); - - if (matchers.isEmpty()) { - return NONE; - } else if (matchers.size() == 1) { - return matchers.stream().findFirst().get(); - } - return new MatcherCombiner(matchers); - } - - public static WildcardMatcher from(Collection collection, boolean caseSensitive) { - if (collection == null || collection.isEmpty()) { - return NONE; - } else if (collection.size() == 1) { - T t = collection.stream().findFirst().get(); - if (t instanceof String) { - return from(((String) t), caseSensitive); - } else if (t instanceof WildcardMatcher) { - return ((WildcardMatcher) t); - } - throw new UnsupportedOperationException("WildcardMatcher can't be constructed from " + t.getClass().getSimpleName()); - } - return from(collection.stream(), caseSensitive); - } - - public static WildcardMatcher from(String[] patterns, boolean caseSensitive) { - if (patterns == null || patterns.length == 0) { - return NONE; - } else if (patterns.length == 1) { - return from(patterns[0], caseSensitive); - } - return from(Arrays.stream(patterns), caseSensitive); - } - - public static WildcardMatcher from(Stream patterns) { - return from(patterns, true); - } - - public static WildcardMatcher from(Collection patterns) { - return from(patterns, true); - } - - public static WildcardMatcher from(String... patterns) { - return from(patterns, true); - } - - public WildcardMatcher concat(Stream matchers) { - return new MatcherCombiner(Stream.concat(matchers, Stream.of(this)).collect(ImmutableSet.toImmutableSet())); - } - - public WildcardMatcher concat(Collection matchers) { - if (matchers.isEmpty()) { - return this; - } - return concat(matchers.stream()); - } - - public WildcardMatcher concat(WildcardMatcher... matchers) { - if (matchers.length == 0) { - return this; - } - return concat(Arrays.stream(matchers)); - } - - public boolean matchAny(Stream candidates) { - return candidates.anyMatch(this); - } - - public boolean matchAny(Collection candidates) { - return matchAny(candidates.stream()); - } - - public boolean matchAny(String... candidates) { - return matchAny(Arrays.stream(candidates)); - } - - public boolean matchAll(Stream candidates) { - return candidates.allMatch(this); - } - - public boolean matchAll(Collection candidates) { - return matchAll(candidates.stream()); - } - - public boolean matchAll(String[] candidates) { - return matchAll(Arrays.stream(candidates)); - } - - public > T getMatchAny(Stream candidates, Collector collector) { - return candidates.filter(this).collect(collector); - } - - public > T getMatchAny(Collection candidate, Collector collector) { - return getMatchAny(candidate.stream(), collector); - } - - public > T getMatchAny(final String[] candidate, Collector collector) { - return getMatchAny(Arrays.stream(candidate), collector); - } - - public Optional findFirst(final String candidate) { - return Optional.ofNullable(test(candidate) ? this : null); - } - - public Iterable iterateMatching(Iterable candidates) { - return iterateMatching(candidates, Function.identity()); - } - - public Iterable iterateMatching(Iterable candidates, Function toStringFunction) { - return new Iterable() { - - @Override - public Iterator iterator() { - Iterator delegate = candidates.iterator(); - - return new Iterator() { - private E next; - - @Override - public boolean hasNext() { - if (next == null) { - init(); - } - - return next != null; - } - - @Override - public E next() { - if (next == null) { - init(); - } - - E result = next; - next = null; - return result; - } - - private void init() { - while (delegate.hasNext()) { - E candidate = delegate.next(); - - if (test(toStringFunction.apply(candidate))) { - next = candidate; - break; - } - } - } - }; - } - }; - } - - public static List matchers(Collection patterns) { - return patterns.stream().map(p -> WildcardMatcher.from(p, true)).collect(Collectors.toList()); - } - - public static List getAllMatchingPatterns(final Collection matchers, final String candidate) { - return matchers.stream().filter(p -> p.test(candidate)).map(Objects::toString).collect(Collectors.toList()); - } - - public static List getAllMatchingPatterns(final Collection pattern, final Collection candidates) { - return pattern.stream().filter(p -> p.matchAny(candidates)).map(Objects::toString).collect(Collectors.toList()); - } - - public static boolean isExact(String pattern) { - return pattern == null || !(pattern.contains("*") || pattern.contains("?") || (pattern.startsWith("/") && pattern.endsWith("/"))); - } - - // - // --- Implementation specializations --- - // - // Casefolding matcher - sits on top of case-sensitive matcher - // and proxies toLower() of input string to the wrapped matcher - private static final class CasefoldingMatcher extends WildcardMatcher { - - private final WildcardMatcher inner; - - public CasefoldingMatcher(String pattern, Function simpleWildcardMatcher) { - this.inner = simpleWildcardMatcher.apply(pattern.toLowerCase()); - } - - @Override - public boolean test(String candidate) { - return inner.test(candidate.toLowerCase()); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CasefoldingMatcher that = (CasefoldingMatcher) o; - return inner.equals(that.inner); - } - - @Override - public int hashCode() { - return inner.hashCode(); - } - - @Override - public String toString() { - return inner.toString(); - } - } - - public static final class Exact extends WildcardMatcher { - - private final String pattern; - - private Exact(String pattern) { - this.pattern = pattern; - } - - @Override - public boolean test(String candidate) { - return pattern.equals(candidate); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Exact that = (Exact) o; - return pattern.equals(that.pattern); - } - - @Override - public int hashCode() { - return pattern.hashCode(); - } - - @Override - public String toString() { - return pattern; - } - } - - // RegexMatcher uses JDK Pattern to test for matching, - // assumes "//" strings as input pattern - private static final class RegexMatcher extends WildcardMatcher { - - private final Pattern pattern; - - private RegexMatcher(String pattern, boolean caseSensitive) { - Preconditions.checkArgument(pattern.length() > 1 && pattern.startsWith("/") && pattern.endsWith("/")); - final String stripSlashesPattern = pattern.substring(1, pattern.length() - 1); - this.pattern = caseSensitive - ? Pattern.compile(stripSlashesPattern) - : Pattern.compile(stripSlashesPattern, Pattern.CASE_INSENSITIVE); - } - - @Override - public boolean test(String candidate) { - return pattern.matcher(candidate).matches(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RegexMatcher that = (RegexMatcher) o; - return pattern.pattern().equals(that.pattern.pattern()); - } - - @Override - public int hashCode() { - return pattern.pattern().hashCode(); - } - - @Override - public String toString() { - return "/" + pattern.pattern() + "/"; - } - } - - // Simple implementation of WildcardMatcher matcher with * and ? without - // using exlicit stack or recursion (as long as we don't need sub-matches it does work) - // allows us to save on resources and heap allocations unless Regex is required - private static final class SimpleMatcher extends WildcardMatcher { - - private final String pattern; - - SimpleMatcher(String pattern) { - this.pattern = pattern; - } - - @Override - public boolean test(String candidate) { - int i = 0; - int j = 0; - int n = candidate.length(); - int m = pattern.length(); - int text_backup = -1; - int wild_backup = -1; - while (i < n) { - if (j < m && pattern.charAt(j) == '*') { - text_backup = i; - wild_backup = ++j; - } else if (j < m && (pattern.charAt(j) == '?' || pattern.charAt(j) == candidate.charAt(i))) { - i++; - j++; - } else { - if (wild_backup == -1) return false; - i = ++text_backup; - j = wild_backup; - } - } - while (j < m && pattern.charAt(j) == '*') - j++; - return j >= m; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SimpleMatcher that = (SimpleMatcher) o; - return pattern.equals(that.pattern); - } - - @Override - public int hashCode() { - return pattern.hashCode(); - } - - @Override - public String toString() { - return pattern; - } - } - - // MatcherCombiner is a combination of a set of matchers - // matches if any of the set do - // Empty MultiMatcher always returns false - private static final class MatcherCombiner extends WildcardMatcher { - - private final Collection wildcardMatchers; - private final int hashCode; - - MatcherCombiner(Collection wildcardMatchers) { - Preconditions.checkArgument(wildcardMatchers.size() > 1); - this.wildcardMatchers = wildcardMatchers; - hashCode = wildcardMatchers.hashCode(); - } - - @Override - public boolean test(String candidate) { - return wildcardMatchers.stream().anyMatch(m -> m.test(candidate)); - } - - @Override - public Optional findFirst(final String candidate) { - return wildcardMatchers.stream().filter(m -> m.test(candidate)).findFirst(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MatcherCombiner that = (MatcherCombiner) o; - return wildcardMatchers.equals(that.wildcardMatchers); - } - - @Override - public int hashCode() { - return hashCode; - } - - @Override - public String toString() { - return wildcardMatchers.toString(); - } - } -} diff --git a/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java b/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java deleted file mode 100644 index 9255b63dba..0000000000 --- a/common/src/main/java/org/opensearch/security/common/user/AuthCredentials.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.user; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; - -import org.opensearch.OpenSearchSecurityException; - -/** - * AuthCredentials are an abstraction to encapsulate credentials like passwords or generic - * native credentials like GSS tokens. - * - */ -public final class AuthCredentials { - - private static final String DIGEST_ALGORITHM = "SHA-256"; - private final String username; - private byte[] password; - private Object nativeCredentials; - private final Set securityRoles = new HashSet(); - private final Set backendRoles = new HashSet(); - private boolean complete; - private final byte[] internalPasswordHash; - private final Map attributes = new HashMap<>(); - - /** - * Create new credentials with a username and native credentials - * - * @param username The username, must not be null or empty - * @param nativeCredentials Arbitrary credentials (like GSS tokens), must not be null - * @throws IllegalArgumentException if username or nativeCredentials are null or empty - */ - public AuthCredentials(final String username, final Object nativeCredentials) { - this(username, null, nativeCredentials); - - if (nativeCredentials == null) { - throw new IllegalArgumentException("nativeCredentials must not be null or empty"); - } - } - - /** - * Create new credentials with a username and password - * - * @param username The username, must not be null or empty - * @param password The password, must not be null or empty - * @throws IllegalArgumentException if username or password is null or empty - */ - public AuthCredentials(final String username, final byte[] password) { - this(username, password, null); - - if (password == null || password.length == 0) { - throw new IllegalArgumentException("password must not be null or empty"); - } - } - - /** - * Create new credentials with a username, a initial optional set of roles and empty password/native credentials - - * @param username The username, must not be null or empty - * @param backendRoles set of roles this user is a member of - * @throws IllegalArgumentException if username is null or empty - */ - public AuthCredentials(final String username, String... backendRoles) { - this(username, null, null, backendRoles); - } - - /** - * Create new credentials with a username, a initial optional set of roles and empty password/native credentials - * @param username The username, must not be null or empty - * @param securityRoles The internal roles the user has been mapped to - * @param backendRoles set of roles this user is a member of - * @throws IllegalArgumentException if username is null or empty - */ - public AuthCredentials(final String username, List securityRoles, String... backendRoles) { - this(username, null, null, backendRoles); - this.securityRoles.addAll(securityRoles); - } - - private AuthCredentials(final String username, byte[] password, Object nativeCredentials, String... backendRoles) { - super(); - - if (username == null || username.isEmpty()) { - throw new IllegalArgumentException("username must not be null or empty"); - } - - this.username = username; - // make defensive copy - this.password = password == null ? null : Arrays.copyOf(password, password.length); - - if (this.password != null) { - try { - MessageDigest digester = MessageDigest.getInstance(DIGEST_ALGORITHM); - internalPasswordHash = digester.digest(this.password); - } catch (NoSuchAlgorithmException e) { - throw new OpenSearchSecurityException("Unable to digest password", e); - } - } else { - internalPasswordHash = null; - } - - if (password != null) { - Arrays.fill(password, (byte) '\0'); - password = null; - } - - this.nativeCredentials = nativeCredentials; - nativeCredentials = null; - - if (backendRoles != null && backendRoles.length > 0) { - this.backendRoles.addAll(Arrays.asList(backendRoles)); - } - } - - /** - * Wipe password and native credentials - */ - public void clearSecrets() { - if (password != null) { - Arrays.fill(password, (byte) '\0'); - password = null; - } - - nativeCredentials = null; - } - - public String getUsername() { - return username; - } - - /** - * - * @return Defensive copy of the password - */ - public byte[] getPassword() { - // make defensive copy - return password == null ? null : Arrays.copyOf(password, password.length); - } - - public Object getNativeCredentials() { - return nativeCredentials; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + Arrays.hashCode(internalPasswordHash); - result = prime * result + ((username == null) ? 0 : username.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) return true; - if (obj == null) return false; - if (getClass() != obj.getClass()) return false; - AuthCredentials other = (AuthCredentials) obj; - if (internalPasswordHash == null - || other.internalPasswordHash == null - || !MessageDigest.isEqual(internalPasswordHash, other.internalPasswordHash)) return false; - if (username == null) { - if (other.username != null) return false; - } else if (!username.equals(other.username)) return false; - return true; - } - - @Override - public String toString() { - return "AuthCredentials [username=" - + username - + ", password empty=" - + (password == null) - + ", nativeCredentials empty=" - + (nativeCredentials == null) - + ",backendRoles=" - + backendRoles - + "]"; - } - - /** - * - * @return Defensive copy of the roles this user is member of. - */ - public Set getBackendRoles() { - return new HashSet(backendRoles); - } - - /** - * - * @return Defensive copy of the security roles this user is member of. - */ - public Set getSecurityRoles() { - return Set.copyOf(securityRoles); - } - - public boolean isComplete() { - return complete; - } - - /** - * If the credentials are complete and no further roundtrips with the originator are due - * then this method must be called so that the authentication flow can proceed. - *

- * If this credentials are already marked a complete then a call to this method does nothing. - * - * @return this - */ - public AuthCredentials markComplete() { - this.complete = true; - return this; - } - - public void addAttribute(String name, String value) { - if (name != null && !name.isEmpty()) { - this.attributes.put(name, value); - } - } - - public Map getAttributes() { - return Collections.unmodifiableMap(this.attributes); - } -} diff --git a/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java b/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java deleted file mode 100644 index 144bb04002..0000000000 --- a/common/src/main/java/org/opensearch/security/common/user/CustomAttributesAware.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.user; - -import java.util.Map; - -public interface CustomAttributesAware { - - Map getCustomAttributesMap(); -} diff --git a/common/src/main/java/org/opensearch/security/common/user/User.java b/common/src/main/java/org/opensearch/security/common/user/User.java deleted file mode 100644 index 015ddf7fb1..0000000000 --- a/common/src/main/java/org/opensearch/security/common/user/User.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Copyright 2015-2018 _floragunn_ GmbH - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.common.user; - -import java.io.IOException; -import java.io.Serializable; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Set; - -import com.google.common.collect.Lists; - -import org.opensearch.core.common.io.stream.StreamInput; -import org.opensearch.core.common.io.stream.StreamOutput; -import org.opensearch.core.common.io.stream.Writeable; - -/** - * A authenticated user and attributes associated to them (like roles, tenant, custom attributes) - *

- * Do not subclass from this class! - */ -public class User implements Serializable, Writeable, CustomAttributesAware { - - public static final User ANONYMOUS = new User( - "opendistro_security_anonymous", - Lists.newArrayList("opendistro_security_anonymous_backendrole"), - null - ); - - // This is a default user that is injected into a transport request when a user info is not present and passive_intertransport_auth is - // enabled. - // This is to be used in scenarios where some of the nodes do not have security enabled, and therefore do not pass any user information - // in threadcontext, yet we need the communication to not break between the nodes. - // Attach the required permissions to either the user or the backend role. - public static final User DEFAULT_TRANSPORT_USER = new User( - "opendistro_security_default_transport_user", - Lists.newArrayList("opendistro_security_default_transport_backendrole"), - null - ); - - private static final long serialVersionUID = -5500938501822658596L; - private final String name; - /** - * roles == backend_roles - */ - private final Set roles = Collections.synchronizedSet(new HashSet()); - private final Set securityRoles = Collections.synchronizedSet(new HashSet()); - private String requestedTenant; - private Map attributes = Collections.synchronizedMap(new HashMap<>()); - private boolean isInjected = false; - - public User(final StreamInput in) throws IOException { - super(); - name = in.readString(); - roles.addAll(in.readList(StreamInput::readString)); - requestedTenant = in.readString(); - if (requestedTenant.isEmpty()) { - requestedTenant = null; - } - attributes = Collections.synchronizedMap(in.readMap(StreamInput::readString, StreamInput::readString)); - securityRoles.addAll(in.readList(StreamInput::readString)); - } - - /** - * Create a new authenticated user - * - * @param name The username (must not be null or empty) - * @param roles Roles of which the user is a member off (maybe null) - * @param customAttributes Custom attributes associated with this (maybe null) - * @throws IllegalArgumentException if name is null or empty - */ - public User(final String name, final Collection roles, final AuthCredentials customAttributes) { - super(); - - if (name == null || name.isEmpty()) { - throw new IllegalArgumentException("name must not be null or empty"); - } - - this.name = name; - - if (roles != null) { - this.addRoles(roles); - } - - if (customAttributes != null) { - this.attributes.putAll(customAttributes.getAttributes()); - } - - } - - /** - * Create a new authenticated user without roles and attributes - * - * @param name The username (must not be null or empty) - * @throws IllegalArgumentException if name is null or empty - */ - public User(final String name) { - this(name, null, null); - } - - public final String getName() { - return name; - } - - /** - * @return A unmodifiable set of the backend roles this user is a member of - */ - public final Set getRoles() { - return Collections.unmodifiableSet(roles); - } - - /** - * Associate this user with a backend role - * - * @param role The backend role - */ - public final void addRole(final String role) { - this.roles.add(role); - } - - /** - * Associate this user with a set of backend roles - * - * @param roles The backend roles - */ - public final void addRoles(final Collection roles) { - if (roles != null) { - this.roles.addAll(roles); - } - } - - /** - * Check if this user is a member of a backend role - * - * @param role The backend role - * @return true if this user is a member of the backend role, false otherwise - */ - public final boolean isUserInRole(final String role) { - return this.roles.contains(role); - } - - /** - * Associate this user with a set of custom attributes - * - * @param attributes custom attributes - */ - public final void addAttributes(final Map attributes) { - if (attributes != null) { - this.attributes.putAll(attributes); - } - } - - public final String getRequestedTenant() { - return requestedTenant; - } - - public final void setRequestedTenant(String requestedTenant) { - this.requestedTenant = requestedTenant; - } - - public boolean isInjected() { - return isInjected; - } - - public void setInjected(boolean isInjected) { - this.isInjected = isInjected; - } - - public final String toStringWithAttributes() { - return "User [name=" - + name - + ", backend_roles=" - + roles - + ", requestedTenant=" - + requestedTenant - + ", attributes=" - + attributes - + "]"; - } - - @Override - public final String toString() { - return "User [name=" + name + ", backend_roles=" + roles + ", requestedTenant=" + requestedTenant + "]"; - } - - @Override - public final int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + (name == null ? 0 : name.hashCode()); - return result; - } - - @Override - public final boolean equals(final Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof User)) { - return false; - } - final User other = (User) obj; - if (name == null) { - if (other.name != null) { - return false; - } - } else if (!name.equals(other.name)) { - return false; - } - return true; - } - - /** - * Copy all backend roles from another user - * - * @param user The user from which the backend roles should be copied over - */ - public final void copyRolesFrom(final User user) { - if (user != null) { - this.addRoles(user.getRoles()); - } - } - - @Override - public void writeTo(StreamOutput out) throws IOException { - out.writeString(name); - out.writeStringCollection(new ArrayList(roles)); - out.writeString(requestedTenant == null ? "" : requestedTenant); - out.writeMap(attributes, StreamOutput::writeString, StreamOutput::writeString); - out.writeStringCollection(securityRoles == null ? Collections.emptyList() : new ArrayList(securityRoles)); - } - - /** - * Get the custom attributes associated with this user - * - * @return A modifiable map with all the current custom attributes associated with this user - */ - public synchronized final Map getCustomAttributesMap() { - if (attributes == null) { - attributes = Collections.synchronizedMap(new HashMap<>()); - } - return attributes; - } - - public final void addSecurityRoles(final Collection securityRoles) { - if (securityRoles != null && this.securityRoles != null) { - this.securityRoles.addAll(securityRoles); - } - } - - public final Set getSecurityRoles() { - return this.securityRoles == null - ? Collections.synchronizedSet(Collections.emptySet()) - : Collections.unmodifiableSet(this.securityRoles); - } - - /** - * Check the custom attributes associated with this user - * - * @return true if it has a service account attributes, otherwise false - */ - public boolean isServiceAccount() { - Map userAttributesMap = this.getCustomAttributesMap(); - return userAttributesMap != null && "true".equals(userAttributesMap.get("attr.internal.service")); - } - - /** - * Check the custom attributes associated with this user - * - * @return true if it has a plugin account attributes, otherwise false - */ - public boolean isPluginUser() { - return name != null && name.startsWith("plugin:"); - } - - public void setAttributes(Map attributes) { - if (attributes == null) { - this.attributes = Collections.synchronizedMap(new HashMap<>()); - } - } -} diff --git a/scripts/build.sh b/scripts/build.sh index e0fa495845..34819e56cc 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -79,7 +79,6 @@ cp ${distributions}/*.zip ./$OUTPUT/plugins # Publish jars ./gradlew :opensearch-resource-sharing-spi:publishToMavenLocal -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER -./gradlew :opensearch-security-common:publishToMavenLocal -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER ./gradlew :opensearch-security-client:publishToMavenLocal -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER ./gradlew publishAllPublicationsToStagingRepository -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -Dbuild.version_qualifier=$QUALIFIER diff --git a/settings.gradle b/settings.gradle index 09be0187cc..f1852a936f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -9,8 +9,5 @@ rootProject.name = 'opensearch-security' include "spi" project(":spi").name = "opensearch-resource-sharing-spi" -include 'common' -project(":common").name = rootProject.name + "-common" - include 'client' project(":client").name = rootProject.name + "-client" diff --git a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java index f560ef713f..f88b2f73ea 100644 --- a/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java +++ b/src/integrationTest/java/org/opensearch/test/framework/cluster/TestRestClient.java @@ -89,8 +89,8 @@ public class TestRestClient implements AutoCloseable { private static final Logger log = LogManager.getLogger(TestRestClient.class); - private boolean enableHTTPClientSSL = true; - private boolean sendHTTPClientCertificate = false; + private boolean enableHTTPClientSSL; + private boolean sendHTTPClientCertificate; private InetSocketAddress nodeHttpAddress; private RequestConfig requestConfig; private List

headers = new ArrayList<>(); @@ -99,11 +99,20 @@ public class TestRestClient implements AutoCloseable { private final InetAddress sourceInetAddress; - public TestRestClient(InetSocketAddress nodeHttpAddress, List
headers, SSLContext sslContext, InetAddress sourceInetAddress) { + public TestRestClient( + InetSocketAddress nodeHttpAddress, + List
headers, + SSLContext sslContext, + InetAddress sourceInetAddress, + boolean enableHTTPClientSSL, + boolean sendHTTPClientCertificate + ) { this.nodeHttpAddress = nodeHttpAddress; this.headers.addAll(headers); this.sslContext = sslContext; this.sourceInetAddress = sourceInetAddress; + this.enableHTTPClientSSL = enableHTTPClientSSL; + this.sendHTTPClientCertificate = sendHTTPClientCertificate; } public HttpResponse get(String path, Header... headers) { diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 56fda88a42..e16cf96c72 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -144,16 +144,6 @@ import org.opensearch.security.auditlog.config.AuditConfig.Filter.FilterEntries; import org.opensearch.security.auditlog.impl.AuditLogImpl; import org.opensearch.security.auth.BackendRegistry; -import org.opensearch.security.common.resources.ResourceAccessHandler; -import org.opensearch.security.common.resources.ResourceIndexListener; -import org.opensearch.security.common.resources.ResourcePluginInfo; -import org.opensearch.security.common.resources.ResourceProvider; -import org.opensearch.security.common.resources.ResourceSharingConstants; -import org.opensearch.security.common.resources.ResourceSharingIndexHandler; -import org.opensearch.security.common.resources.ResourceSharingIndexManagementRepository; -import org.opensearch.security.common.resources.rest.ResourceAccessAction; -import org.opensearch.security.common.resources.rest.ResourceAccessRestAction; -import org.opensearch.security.common.resources.rest.ResourceAccessTransportAction; import org.opensearch.security.compliance.ComplianceIndexingOperationListener; import org.opensearch.security.compliance.ComplianceIndexingOperationListenerImpl; import org.opensearch.security.configuration.AdminDNs; @@ -184,6 +174,16 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourceIndexListener; +import org.opensearch.security.resources.ResourcePluginInfo; +import org.opensearch.security.resources.ResourceProvider; +import org.opensearch.security.resources.ResourceSharingConstants; +import org.opensearch.security.resources.ResourceSharingIndexHandler; +import org.opensearch.security.resources.ResourceSharingIndexManagementRepository; +import org.opensearch.security.resources.rest.ResourceAccessAction; +import org.opensearch.security.resources.rest.ResourceAccessRestAction; +import org.opensearch.security.resources.rest.ResourceAccessTransportAction; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; @@ -273,7 +273,6 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile RestLayerPrivilegesEvaluator restLayerEvaluator; private volatile ConfigurationRepository cr; private volatile AdminDNs adminDns; - private volatile org.opensearch.security.common.configuration.AdminDNs adminDNsCommon; private volatile ClusterService cs; private volatile AtomicReference localNode = new AtomicReference<>(); private volatile AuditLog auditLog; @@ -754,6 +753,7 @@ public void onIndexModule(IndexModule indexModule) { // Listening on POST and DELETE operations in resource indices ResourceIndexListener resourceIndexListener = ResourceIndexListener.getInstance(); resourceIndexListener.initialize(threadPool, localClient); + if (settings.getAsBoolean( ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED, ConfigConstants.OPENSEARCH_RESOURCE_SHARING_ENABLED_DEFAULT @@ -1138,7 +1138,6 @@ public Collection createComponents( sslExceptionHandler = new AuditLogSslExceptionHandler(auditLog); adminDns = new AdminDNs(settings); - adminDNsCommon = new org.opensearch.security.common.configuration.AdminDNs(settings); cr = ConfigurationRepository.create(settings, this.configPath, threadPool, localClient, clusterService, auditLog); @@ -1169,7 +1168,7 @@ public Collection createComponents( final var resourceSharingIndex = ResourceSharingConstants.OPENSEARCH_RESOURCE_SHARING_INDEX; ResourceSharingIndexHandler rsIndexHandler = new ResourceSharingIndexHandler(resourceSharingIndex, localClient, threadPool); - ResourceAccessHandler resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDNsCommon); + ResourceAccessHandler resourceAccessHandler = new ResourceAccessHandler(threadPool, rsIndexHandler, adminDns); resourceAccessHandler.initializeRecipientTypes(); // Resource Sharing index is enabled by default boolean isResourceSharingEnabled = settings.getAsBoolean( @@ -1258,7 +1257,6 @@ public Collection createComponents( } components.add(adminDns); - components.add(adminDNsCommon); components.add(cr); components.add(xffResolver); components.add(backendRegistry); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index a8295a410f..63ded48659 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -58,7 +58,6 @@ import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.auth.internal.NoOpAuthenticationBackend; -import org.opensearch.security.common.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.filter.SecurityRequest; import org.opensearch.security.filter.SecurityRequestChannel; @@ -225,7 +224,7 @@ public boolean authenticate(final SecurityRequestChannel request) { if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call User superuser = new User(sslPrincipal); - UserSubject subject = new UserSubjectImpl(threadPool, new org.opensearch.security.common.user.User(sslPrincipal)); + UserSubject subject = new UserSubjectImpl(threadPool, superuser); threadContext.putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); return true; @@ -392,15 +391,9 @@ public boolean authenticate(final SecurityRequestChannel request) { final User impersonatedUser = impersonate(request, authenticatedUser); final User effectiveUser = impersonatedUser == null ? authenticatedUser : impersonatedUser; threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, effectiveUser); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_INITIATING_USER, authenticatedUser.getName()); - // TODO: The following artistry must be reverted when User class is completely moved to :opensearch-security-common - org.opensearch.security.common.user.User effUser = new org.opensearch.security.common.user.User( - effectiveUser.getName(), - effectiveUser.getRoles(), - null - ); - effUser.setAttributes(effectiveUser.getCustomAttributesMap()); - UserSubject subject = new UserSubjectImpl(threadPool, effUser); + UserSubject subject = new UserSubjectImpl(threadPool, effectiveUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); } else { if (isDebugEnabled) { @@ -428,14 +421,7 @@ public boolean authenticate(final SecurityRequestChannel request) { User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); - org.opensearch.security.common.user.User anonymousUserCommon = new org.opensearch.security.common.user.User( - User.ANONYMOUS.getName(), - new HashSet<>(User.ANONYMOUS.getRoles()), - null - ); - anonymousUserCommon.setRequestedTenant(tenant); - - UserSubject subject = new UserSubjectImpl(threadPool, anonymousUserCommon); + UserSubject subject = new UserSubjectImpl(threadPool, anonymousUser); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, subject); diff --git a/common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java b/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java similarity index 90% rename from common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java rename to src/main/java/org/opensearch/security/auth/UserSubjectImpl.java index 620250be53..3f3710ca7d 100644 --- a/common/src/main/java/org/opensearch/security/common/auth/UserSubjectImpl.java +++ b/src/main/java/org/opensearch/security/auth/UserSubjectImpl.java @@ -7,7 +7,7 @@ * compatible open source license. * */ -package org.opensearch.security.common.auth; +package org.opensearch.security.auth; import java.security.Principal; import java.util.concurrent.Callable; @@ -16,8 +16,8 @@ import org.opensearch.identity.NamedPrincipal; import org.opensearch.identity.UserSubject; import org.opensearch.identity.tokens.AuthToken; -import org.opensearch.security.common.support.ConfigConstants; -import org.opensearch.security.common.user.User; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; public class UserSubjectImpl implements UserSubject { diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index 58d6f77d0b..dcf2156bf4 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -51,11 +51,13 @@ import org.opensearch.security.user.User; import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; -import static org.opensearch.security.OpenSearchSecurityPlugin.LEGACY_OPENDISTRO_PREFIX; -import static org.opensearch.security.OpenSearchSecurityPlugin.PLUGINS_PREFIX; public class Utils { + @Deprecated + public static final String LEGACY_OPENDISTRO_PREFIX = "_opendistro/_security"; + public static final String PLUGINS_PREFIX = "_plugins/_security"; + public final static String PLUGIN_ROUTE_PREFIX = "/" + PLUGINS_PREFIX; @Deprecated diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java similarity index 97% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java rename to src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 3912001aa1..0053e29d06 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.resources; import java.util.Collections; import java.util.HashSet; @@ -22,10 +22,8 @@ import org.opensearch.action.StepListener; import org.opensearch.common.util.concurrent.ThreadContext; import org.opensearch.core.action.ActionListener; -import org.opensearch.security.common.auth.UserSubjectImpl; -import org.opensearch.security.common.configuration.AdminDNs; -import org.opensearch.security.common.support.ConfigConstants; -import org.opensearch.security.common.user.User; +import org.opensearch.security.auth.UserSubjectImpl; +import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; @@ -35,6 +33,8 @@ import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.security.spi.resources.sharing.ShareWith; import org.opensearch.security.spi.resources.sharing.SharedWithScope; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; /** @@ -175,7 +175,10 @@ public void getAccessibleResourcesForCurrentUser(String res try { validateArguments(resourceIndex); - ResourceParser parser = ResourcePluginInfo.getInstance().getResourceProviders().get(resourceIndex).resourceParser(); + ResourceParser parser = (ResourceParser) ResourcePluginInfo.getInstance() + .getResourceProviders() + .get(resourceIndex) + .resourceParser(); StepListener> resourceIdsListener = new StepListener<>(); StepListener> resourcesListener = new StepListener<>(); @@ -227,7 +230,7 @@ public void hasPermission(String resourceId, String resourceIndex, Set s return; } - LOGGER.debug("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); + LOGGER.info("Checking if user '{}' has '{}' permission to resource '{}'", user.getName(), scopes.toString(), resourceId); if (adminDNs.isAdmin(user)) { LOGGER.debug( diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java similarity index 95% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java rename to src/main/java/org/opensearch/security/resources/ResourceIndexListener.java index 4b502096b4..82eb127d45 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceIndexListener.java +++ b/src/main/java/org/opensearch/security/resources/ResourceIndexListener.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.resources; import java.io.IOException; import java.util.Objects; @@ -18,12 +18,12 @@ import org.opensearch.core.index.shard.ShardId; import org.opensearch.index.engine.Engine; import org.opensearch.index.shard.IndexingOperationListener; -import org.opensearch.security.common.auth.UserSubjectImpl; -import org.opensearch.security.common.support.ConfigConstants; -import org.opensearch.security.common.user.User; +import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.spi.resources.sharing.CreatedBy; import org.opensearch.security.spi.resources.sharing.Creator; import org.opensearch.security.spi.resources.sharing.ResourceSharing; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.client.Client; diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java similarity index 87% rename from common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java rename to src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java index fde006c198..8827555302 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourcePluginInfo.java +++ b/src/main/java/org/opensearch/security/resources/ResourcePluginInfo.java @@ -1,4 +1,12 @@ -package org.opensearch.security.common.resources; +/* + * 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.resources; import java.util.HashMap; import java.util.HashSet; diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java b/src/main/java/org/opensearch/security/resources/ResourceProvider.java similarity index 75% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java rename to src/main/java/org/opensearch/security/resources/ResourceProvider.java index b2537fc849..44936c5541 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceProvider.java +++ b/src/main/java/org/opensearch/security/resources/ResourceProvider.java @@ -6,8 +6,9 @@ * compatible open source license. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.resources; +import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; /** @@ -16,6 +17,6 @@ * * @opensearch.experimental */ -public record ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { +public record ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { } diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java b/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java similarity index 92% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java rename to src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java index a1004566e5..89bdcda02a 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingConstants.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingConstants.java @@ -8,7 +8,7 @@ * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.resources; /** * This class contains constants related to resource sharing in OpenSearch. diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java similarity index 99% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java rename to src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 8ff771d74e..54174a874a 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -7,7 +7,7 @@ * compatible open source license. * */ -package org.opensearch.security.common.resources; +package org.opensearch.security.resources; import java.io.IOException; import java.util.ArrayList; @@ -63,7 +63,7 @@ import org.opensearch.search.Scroll; import org.opensearch.search.SearchHit; import org.opensearch.search.builder.SearchSourceBuilder; -import org.opensearch.security.common.DefaultObjectMapper; +import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.spi.resources.Resource; import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; diff --git a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java similarity index 97% rename from common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java rename to src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java index 166d410f86..95c1b49357 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/ResourceSharingIndexManagementRepository.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexManagementRepository.java @@ -9,7 +9,7 @@ * GitHub history for details. */ -package org.opensearch.security.common.resources; +package org.opensearch.security.resources; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessAction.java similarity index 93% rename from common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java rename to src/main/java/org/opensearch/security/resources/rest/ResourceAccessAction.java index 5820d21a8c..3144751711 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessAction.java +++ b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources.rest; +package org.opensearch.security.resources.rest; import org.opensearch.action.ActionType; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessRequest.java similarity index 99% rename from common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java rename to src/main/java/org/opensearch/security/resources/rest/ResourceAccessRequest.java index 1df9c244bb..a787f1ec04 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequest.java +++ b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessRequest.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources.rest; +package org.opensearch.security.resources.rest; import java.io.IOException; import java.util.Collection; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessRequestParams.java similarity index 93% rename from common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java rename to src/main/java/org/opensearch/security/resources/rest/ResourceAccessRequestParams.java index 880cfe00ec..68fff8c38b 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRequestParams.java +++ b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessRequestParams.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources.rest; +package org.opensearch.security.resources.rest; import java.io.IOException; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessResponse.java similarity index 98% rename from common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java rename to src/main/java/org/opensearch/security/resources/rest/ResourceAccessResponse.java index ac3ebf602f..b59cee4749 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessResponse.java +++ b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessResponse.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources.rest; +package org.opensearch.security.resources.rest; import java.io.IOException; import java.util.Collections; diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessRestAction.java similarity index 83% rename from common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java rename to src/main/java/org/opensearch/security/resources/rest/ResourceAccessRestAction.java index 700a064ed5..8cb3c38906 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessRestAction.java +++ b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessRestAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources.rest; +package org.opensearch.security.resources.rest; import java.io.IOException; import java.util.HashMap; @@ -28,16 +28,16 @@ import static org.opensearch.rest.RestRequest.Method.GET; import static org.opensearch.rest.RestRequest.Method.POST; -import static org.opensearch.security.common.dlic.rest.api.Responses.badRequest; -import static org.opensearch.security.common.dlic.rest.api.Responses.forbidden; -import static org.opensearch.security.common.dlic.rest.api.Responses.ok; -import static org.opensearch.security.common.dlic.rest.api.Responses.unauthorized; -import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.LIST; -import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.REVOKE; -import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.SHARE; -import static org.opensearch.security.common.resources.rest.ResourceAccessRequest.Operation.VERIFY; -import static org.opensearch.security.common.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; -import static org.opensearch.security.common.support.Utils.addRoutesPrefix; +import static org.opensearch.security.dlic.rest.api.Responses.badRequest; +import static org.opensearch.security.dlic.rest.api.Responses.forbidden; +import static org.opensearch.security.dlic.rest.api.Responses.ok; +import static org.opensearch.security.dlic.rest.api.Responses.unauthorized; +import static org.opensearch.security.dlic.rest.support.Utils.PLUGIN_RESOURCE_ROUTE_PREFIX; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; +import static org.opensearch.security.resources.rest.ResourceAccessRequest.Operation.LIST; +import static org.opensearch.security.resources.rest.ResourceAccessRequest.Operation.REVOKE; +import static org.opensearch.security.resources.rest.ResourceAccessRequest.Operation.SHARE; +import static org.opensearch.security.resources.rest.ResourceAccessRequest.Operation.VERIFY; /** * This class handles the REST API for resource access management. diff --git a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessTransportAction.java similarity index 97% rename from common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java rename to src/main/java/org/opensearch/security/resources/rest/ResourceAccessTransportAction.java index 6bd58246c8..fb4a8ea0d7 100644 --- a/common/src/main/java/org/opensearch/security/common/resources/rest/ResourceAccessTransportAction.java +++ b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessTransportAction.java @@ -6,7 +6,7 @@ * compatible open source license. */ -package org.opensearch.security.common.resources.rest; +package org.opensearch.security.resources.rest; import java.util.Map; import java.util.Set; @@ -17,7 +17,7 @@ import org.opensearch.common.inject.Inject; import org.opensearch.core.action.ActionListener; import org.opensearch.indices.SystemIndices; -import org.opensearch.security.common.resources.ResourceAccessHandler; +import org.opensearch.security.resources.ResourceAccessHandler; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.security.spi.resources.sharing.RecipientType; import org.opensearch.security.spi.resources.sharing.RecipientTypeRegistry; diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 346437e775..811639b74e 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -45,6 +45,9 @@ 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 OPENSEARCH_SECURITY_DISABLED = SECURITY_SETTINGS_PREFIX + "disabled"; + public static final boolean OPENSEARCH_SECURITY_DISABLED_DEFAULT = false; + public static final String OPENDISTRO_SECURITY_CHANNEL_TYPE = OPENDISTRO_SECURITY_CONFIG_PREFIX + "channel_type"; public static final String OPENDISTRO_SECURITY_ORIGIN = OPENDISTRO_SECURITY_CONFIG_PREFIX + "origin"; @@ -119,10 +122,10 @@ public class ConfigConstants { // persistent header. This header is set once and cannot be stashed public static final String OPENDISTRO_SECURITY_AUTHENTICATED_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "authenticated_user"; - public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; - public static final String OPENDISTRO_SECURITY_INITIATING_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "_initiating_user"; + public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; + public static final String OPENDISTRO_SECURITY_INJECTED_USER = "injected_user"; public static final String OPENDISTRO_SECURITY_INJECTED_USER_HEADER = "injected_user_header"; diff --git a/src/main/java/org/opensearch/security/support/ConfigHelper.java b/src/main/java/org/opensearch/security/support/ConfigHelper.java index d2cb6fc8ff..f56a7e5db2 100644 --- a/src/main/java/org/opensearch/security/support/ConfigHelper.java +++ b/src/main/java/org/opensearch/security/support/ConfigHelper.java @@ -49,7 +49,6 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.core.xcontent.XContentParser; import org.opensearch.index.engine.VersionConflictEngineException; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.Meta; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; diff --git a/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java b/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java index d4d0c3f872..deaf0ec031 100644 --- a/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java +++ b/src/main/java/org/opensearch/security/support/SecurityIndexHandler.java @@ -40,7 +40,6 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.configuration.ConfigurationMap; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; diff --git a/src/main/java/org/opensearch/security/support/SecurityJsonNode.java b/src/main/java/org/opensearch/security/support/SecurityJsonNode.java index ffd4fbd68a..2b273ceddb 100644 --- a/src/main/java/org/opensearch/security/support/SecurityJsonNode.java +++ b/src/main/java/org/opensearch/security/support/SecurityJsonNode.java @@ -25,8 +25,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.JsonNodeType; -import org.opensearch.security.DefaultObjectMapper; - public final class SecurityJsonNode { private final JsonNode node; diff --git a/src/main/java/org/opensearch/security/support/WildcardMatcher.java b/src/main/java/org/opensearch/security/support/WildcardMatcher.java index 537e2d473c..26c7dd71bc 100644 --- a/src/main/java/org/opensearch/security/support/WildcardMatcher.java +++ b/src/main/java/org/opensearch/security/support/WildcardMatcher.java @@ -226,7 +226,7 @@ public static WildcardMatcher from(String... patterns) { } public WildcardMatcher concat(Stream matchers) { - return new WildcardMatcher.MatcherCombiner(Stream.concat(matchers, Stream.of(this)).collect(ImmutableSet.toImmutableSet())); + return new MatcherCombiner(Stream.concat(matchers, Stream.of(this)).collect(ImmutableSet.toImmutableSet())); } public WildcardMatcher concat(Collection matchers) { diff --git a/src/main/java/org/opensearch/security/support/YamlConfigReader.java b/src/main/java/org/opensearch/security/support/YamlConfigReader.java index c8096f744c..4ebe1e0a5c 100644 --- a/src/main/java/org/opensearch/security/support/YamlConfigReader.java +++ b/src/main/java/org/opensearch/security/support/YamlConfigReader.java @@ -25,7 +25,6 @@ import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.common.bytes.BytesReference; import org.opensearch.core.xcontent.NamedXContentRegistry; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.securityconf.impl.Meta; import org.opensearch.security.securityconf.impl.SecurityDynamicConfiguration; diff --git a/src/main/java/org/opensearch/security/user/User.java b/src/main/java/org/opensearch/security/user/User.java index 190729623d..dcad5abb57 100644 --- a/src/main/java/org/opensearch/security/user/User.java +++ b/src/main/java/org/opensearch/security/user/User.java @@ -46,7 +46,6 @@ * A authenticated user and attributes associated to them (like roles, tenant, custom attributes) *

* Do not subclass from this class! - * */ public class User implements Serializable, Writeable, CustomAttributesAware { @@ -93,8 +92,8 @@ public User(final StreamInput in) throws IOException { /** * Create a new authenticated user * - * @param name The username (must not be null or empty) - * @param roles Roles of which the user is a member off (maybe null) + * @param name The username (must not be null or empty) + * @param roles Roles of which the user is a member off (maybe null) * @param customAttributes Custom attributes associated with this (maybe null) * @throws IllegalArgumentException if name is null or empty */ @@ -132,7 +131,6 @@ public final String getName() { } /** - * * @return A unmodifiable set of the backend roles this user is a member of */ public final Set getRoles() { @@ -305,4 +303,10 @@ public boolean isServiceAccount() { public boolean isPluginUser() { return name != null && name.startsWith("plugin:"); } + + public void setAttributes(Map attributes) { + if (attributes == null) { + this.attributes = Collections.synchronizedMap(new HashMap<>()); + } + } } diff --git a/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java b/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java index 07bac9e349..9e630ef750 100644 --- a/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java +++ b/src/test/java/org/opensearch/security/auth/UserSubjectImplTests.java @@ -15,8 +15,7 @@ import org.junit.Test; -import org.opensearch.security.common.auth.UserSubjectImpl; -import org.opensearch.security.common.user.User; +import org.opensearch.security.user.User; import org.opensearch.threadpool.TestThreadPool; import org.opensearch.threadpool.ThreadPool; diff --git a/src/test/java/org/opensearch/security/support/ConfigReaderTest.java b/src/test/java/org/opensearch/security/support/ConfigReaderTest.java index dfdfa76e75..9df231cf2b 100644 --- a/src/test/java/org/opensearch/security/support/ConfigReaderTest.java +++ b/src/test/java/org/opensearch/security/support/ConfigReaderTest.java @@ -18,7 +18,6 @@ import org.junit.Test; import org.junit.rules.TemporaryFolder; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.securityconf.impl.CType; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java b/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java index 5befb21012..bf4b00d8df 100644 --- a/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java +++ b/src/test/java/org/opensearch/security/support/SecurityIndexHandlerTest.java @@ -43,7 +43,6 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.core.common.bytes.BytesArray; import org.opensearch.index.get.GetResult; -import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.configuration.ConfigurationMap; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.state.SecurityConfig; From 33c2dc4b9371aa7902a7680b095277f3a052aba6 Mon Sep 17 00:00:00 2001 From: Darshit Chanpura Date: Thu, 20 Mar 2025 13:49:37 -0400 Subject: [PATCH 7/7] Conforms to SPI file name changes and removes spi package Signed-off-by: Darshit Chanpura --- RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md | 14 +- client/README.md | 14 +- client/build.gradle | 2 +- .../resources/ResourceSharingClient.java | 4 +- .../resources/ResourceSharingNodeClient.java | 6 +- spi/README.md | 167 --------- spi/build.gradle | 86 ----- .../security/spi/resources/Resource.java | 27 -- .../spi/resources/ResourceAccessScope.java | 38 --- .../spi/resources/ResourceParser.java | 29 -- .../resources/ResourceSharingExtension.java | 35 -- .../exceptions/ResourceSharingException.java | 60 ---- .../security/spi/resources/package-info.java | 15 - .../spi/resources/sharing/CreatedBy.java | 88 ----- .../spi/resources/sharing/Creator.java | 37 -- .../spi/resources/sharing/Recipient.java | 31 -- .../spi/resources/sharing/RecipientType.java | 24 -- .../sharing/RecipientTypeRegistry.java | 39 --- .../resources/sharing/ResourceSharing.java | 202 ----------- .../spi/resources/sharing/ShareWith.java | 103 ------ .../resources/sharing/SharedWithScope.java | 169 --------- .../spi/resources/CreatedByTests.java | 320 ------------------ .../resources/RecipientTypeRegistryTests.java | 43 --- .../spi/resources/ShareWithTests.java | 284 ---------------- .../security/OpenSearchSecurityPlugin.java | 8 +- .../resources/ResourceAccessHandler.java | 16 +- .../security/resources/ResourceProvider.java | 7 +- .../ResourceSharingIndexHandler.java | 10 +- .../rest/ResourceAccessResponse.java | 12 +- 29 files changed, 48 insertions(+), 1842 deletions(-) delete mode 100644 spi/README.md delete mode 100644 spi/build.gradle delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/Resource.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/package-info.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java delete mode 100644 spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java delete mode 100644 spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java delete mode 100644 spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java delete mode 100644 spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java diff --git a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md index c537f6a292..4d06795f3f 100644 --- a/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md +++ b/RESOURCE_ACCESS_CONTROL_FOR_PLUGINS.md @@ -8,7 +8,7 @@ The **Resource Sharing and Access Control** feature in OpenSearch Security Plugi - **Super admins** to access all resources. - Plugins to **define and manage resource access** via a standardized interface. -This feature ensures **secure** and **controlled** access to resources while leveraging existing **index-level authorization** in OpenSearch. +This feature ensures **secure** and **controlled** access to shareableResources while leveraging existing **index-level authorization** in OpenSearch. --- @@ -37,7 +37,7 @@ implementation group: 'org.opensearch', name:'opensearch-security-client', versi ``` - **Declare a `compileOnly` dependency** on `opensearch-resource-sharing-spi` package: ```build.gradle -compileOnly "org.opensearch:opensearch-resource-sharing-spi:${opensearch_build}" +compileOnly group: 'org.opensearch', name:'opensearch-resource-sharing-spi', version:"${opensearch_build}" ``` - **Extend** `opensearch-security` plugin with optional flag: ```build.gradle @@ -77,7 +77,7 @@ This feature is controlled by the following flag: ### **Declaring a Plugin as a Resource Plugin** To integrate with the security plugin, your plugin must: 1. Extend `ResourceSharingExtension` and implement required methods. -2. Implement the `Resource` interface for resource declaration. +2. Implement the `ShareableResource` interface for resource declaration. 3. Implement a resource parser to extract resource details. [`opensearch-resource-sharing-spi` README.md](./spi/README.md) is a great resource to learn more about the components of SPI and how to set up. @@ -108,7 +108,7 @@ public class SampleResourcePlugin extends Plugin implements SystemIndexPlugin, R } @Override - public ResourceParser getResourceParser() { + public ShareableResourceParser getShareableResourceParser() { return new SampleResourceParser(); } } @@ -153,7 +153,7 @@ This feature introduces a new **sharing mechanism** called **scopes**. Scopes de Each plugin must **document its scope definitions** so that users understand the **sharing semantics** and how different scopes affect access control. -Scopes enable **granular access control**, allowing resources to be shared with **customized permission levels**, making the system more flexible and adaptable to different use cases. +Scopes enable **granular access control**, allowing shareableResources to be shared with **customized permission levels**, making the system more flexible and adaptable to different use cases. ### **Common Scopes for Plugins to declare** | Scope | Description | @@ -162,7 +162,7 @@ Scopes enable **granular access control**, allowing resources to be shared with | `READ_ONLY` | Users can view but not modify the resource. | | `READ_WRITE` | Users can view and modify the resource. | -By default, all resources are private and only visible to the owner and super-admins. Resources become accessible to others only when explicitly shared. +By default, all shareableResources are private and only visible to the owner and super-admins. Resources become accessible to others only when explicitly shared. SPI provides you an interface, with two default scopes `PUBLIC` and `RESTRICTED`, which can be extended to introduce more plugin-specific values. @@ -451,7 +451,7 @@ Returns an array of accessible resources. --- ## **Conclusion** -The **Resource Sharing and Access Control** feature enhances OpenSearch security by introducing an **additional layer of fine-grained access management** for plugin-defined resources. While **Fine-Grained Access Control (FGAC)** is already enabled, this feature provides **even more granular control** specifically for **resource-level access** within plugins. +The **Resource Sharing and Access Control** feature enhances OpenSearch security by introducing an **additional layer of fine-grained access management** for plugin-defined shareableResources. While **Fine-Grained Access Control (FGAC)** is already enabled, this feature provides **even more granular control** specifically for **resource-level access** within plugins. By implementing the **Service Provider Interface (SPI)**, utilizing the **security client**, and following **best practices**, developers can seamlessly integrate this feature into their plugins to enforce controlled resource sharing and access management. diff --git a/client/README.md b/client/README.md index b37e9ace93..a0e62c3aa4 100644 --- a/client/README.md +++ b/client/README.md @@ -79,7 +79,7 @@ protected void doExecute(Task task, DeleteResourceRequest request, ActionListene ## **Available Java APIs** -The **`ResourceSharingClient`** provides **four Java APIs** for **resource access control**, enabling plugins to **verify, share, revoke, and list** resources. +The **`ResourceSharingClient`** provides **four Java APIs** for **resource access control**, enabling plugins to **verify, share, revoke, and list** shareableResources. **Package Location:** [`org.opensearch.security.client.resources.ResourceSharingClient`](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java) @@ -185,7 +185,7 @@ resourceSharingClient.revokeResourceAccess( --- ### **4. `listAllAccessibleResources`** -**Retrieves all resources the current user has access to.** +**Retrieves all shareableResources the current user has access to.** #### **Method Signature:** ```java @@ -196,16 +196,16 @@ void listAllAccessibleResources(String resourceIndex, ActionListener { - for (Resource resource : resources) { + ActionListener.wrap(shareableResources -> { + for (Resource resource : shareableResources) { System.out.println("Accessible Resource: " + resource.getId()); } }, e -> { - System.err.println("Failed to list accessible resources: " + e.getMessage()); + System.err.println("Failed to list accessible shareableResources: " + e.getMessage()); }) ); ``` -> **Use Case:** Helps a user identify **which resources they can interact with**. +> **Use Case:** Helps a user identify **which shareableResources they can interact with**. --- @@ -214,7 +214,7 @@ These APIs provide essential methods for **fine-grained resource access control* ✔ **Verification** of resource access. ✔ **Granting and revoking** access dynamically. -✔ **Retrieval** of all accessible resources. +✔ **Retrieval** of all accessible shareableResources. For further details, refer to the [`ResourceSharingClient` Java class](../client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java). diff --git a/client/build.gradle b/client/build.gradle index e1876dac24..cf2866403e 100644 --- a/client/build.gradle +++ b/client/build.gradle @@ -34,7 +34,7 @@ repositories { dependencies { compileOnly "org.opensearch:opensearch:${opensearch_version}" - compileOnly project(path: ":opensearch-resource-sharing-spi") + compileOnly group: 'org.opensearch', name:'opensearch-resource-sharing-spi', version:"${opensearch_build}" compileOnly project(":") } diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java index 615f27ed68..de9e4f32ba 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingClient.java @@ -12,7 +12,7 @@ import java.util.Set; import org.opensearch.core.action.ActionListener; -import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ShareableResource; import org.opensearch.security.spi.resources.sharing.ResourceSharing; /** @@ -61,5 +61,5 @@ void revokeResourceAccess( * @param resourceIndex The index containing the resources. * @param listener The listener to be notified with the set of accessible resources. */ - void listAllAccessibleResources(String resourceIndex, ActionListener> listener); + void listAllAccessibleResources(String resourceIndex, ActionListener> listener); } diff --git a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java index 12550284f8..f271416966 100644 --- a/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java +++ b/client/src/main/java/org/opensearch/security/client/resources/ResourceSharingNodeClient.java @@ -20,7 +20,7 @@ import org.opensearch.security.resources.rest.ResourceAccessAction; import org.opensearch.security.resources.rest.ResourceAccessRequest; import org.opensearch.security.resources.rest.ResourceAccessResponse; -import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ShareableResource; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.security.spi.resources.sharing.ResourceSharing; import org.opensearch.security.support.ConfigConstants; @@ -60,7 +60,7 @@ public ResourceSharingNodeClient(Client client, Settings settings) { @Override public void verifyResourceAccess(String resourceId, String resourceIndex, Set scopes, ActionListener listener) { if (isSecurityDisabled || !resourceSharingEnabled) { - String message = isSecurityDisabled ? "Security Plugin is disabled." : "Resource Access Control feature is disabled."; + String message = isSecurityDisabled ? "Security Plugin is disabled." : "ShareableResource Access Control feature is disabled."; log.warn("{} {}", message, "Access to resource is automatically granted"); listener.onResponse(true); @@ -133,7 +133,7 @@ public void revokeResourceAccess( * @param listener The listener to be notified with the set of accessible resources. */ @Override - public void listAllAccessibleResources(String resourceIndex, ActionListener> listener) { + public void listAllAccessibleResources(String resourceIndex, ActionListener> listener) { if (isResourceAccessControlOrSecurityPluginDisabled("Unable to list all accessible resources.", listener)) { return; } diff --git a/spi/README.md b/spi/README.md deleted file mode 100644 index 2d4d13f989..0000000000 --- a/spi/README.md +++ /dev/null @@ -1,167 +0,0 @@ -# **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.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 deleted file mode 100644 index b8f33319b3..0000000000 --- a/spi/build.gradle +++ /dev/null @@ -1,86 +0,0 @@ -/* - * 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/Resource.java b/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java deleted file mode 100644 index 72e0b7b5d1..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/Resource.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.spi.resources; - -import org.opensearch.core.common.io.stream.NamedWriteable; -import org.opensearch.core.xcontent.ToXContentFragment; - -/** - * Marker interface for all resources - * - * @opensearch.experimental - */ -public interface Resource extends NamedWriteable, ToXContentFragment { - /** - * Abstract method to get the resource name. - * Must be implemented by plugins defining resources. - * - * @return resource name - */ - String getResourceName(); -} 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 deleted file mode 100644 index c3b54a8c23..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceAccessScope.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 RESTRICTED = "restricted"; - String PUBLIC = "public"; - - 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/ResourceParser.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java deleted file mode 100644 index b02269322e..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceParser.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.spi.resources; - -import java.io.IOException; - -import org.opensearch.core.xcontent.XContentParser; - -/** - * Interface for parsing resources from XContentParser - * @param the type of resource to be parsed - * - * @opensearch.experimental - */ -public interface ResourceParser { - /** - * Parse source bytes supplied by the parser to a desired Resource type - * @param parser to parser bytes-ref json input - * @return the parsed object of Resource 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/ResourceSharingExtension.java b/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java deleted file mode 100644 index bbfc802d82..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/ResourceSharingExtension.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 - */ - ResourceParser getResourceParser(); -} 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 deleted file mode 100644 index 560669112b..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/exceptions/ResourceSharingException.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - * - * Modifications Copyright OpenSearch Contributors. See - * GitHub history for details. - */ - -package org.opensearch.security.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 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() { - 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/package-info.java b/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java deleted file mode 100644 index f2e210a5e5..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/package-info.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -/** - * 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 deleted file mode 100644 index 50bdd1aea7..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/CreatedBy.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index 75e2415b93..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Creator.java +++ /dev/null @@ -1,37 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index 77215071de..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/Recipient.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index d3b916abc2..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientType.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index a1bdb89089..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/RecipientTypeRegistry.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index 1690213872..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ResourceSharing.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 "Resource {" - + "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 deleted file mode 100644 index 267bb7ece0..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/ShareWith.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index 1dfca103a3..0000000000 --- a/spi/src/main/java/org/opensearch/security/spi/resources/sharing/SharedWithScope.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index 7d6eb5c61a..0000000000 --- a/spi/src/test/java/org/opensearch/security/spi/resources/CreatedByTests.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index 8b0bfa3297..0000000000 --- a/spi/src/test/java/org/opensearch/security/spi/resources/RecipientTypeRegistryTests.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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 deleted file mode 100644 index d7ffa0ce5e..0000000000 --- a/spi/src/test/java/org/opensearch/security/spi/resources/ShareWithTests.java +++ /dev/null @@ -1,284 +0,0 @@ -/* - * SPDX-License-Identifier: Apache-2.0 - * - * The OpenSearch Contributors require contributions made to - * this file be licensed under the Apache-2.0 license or a - * compatible open source license. - */ - -package org.opensearch.security.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.RESTRICTED) - .array("users", "user1", "user2") - .array("roles", "role1") - .array("backend_roles", "backend_role1") - .endObject() - .startObject(ResourceAccessScope.PUBLIC) - .array("users", "user3") - .array("roles", "role2", "role3") - .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.RESTRICTED)) { - 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(2) - ); - MatcherAssert.assertThat( - recipients.get(RecipientTypeRegistry.fromValue(DefaultRecipientType.BACKEND_ROLES.getName())).size(), - is(0) - ); - } - } - } - - @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.RESTRICTED, 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 e16cf96c72..61bf771495 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -194,9 +194,9 @@ import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.setting.TransportPassiveAuthSetting; -import org.opensearch.security.spi.resources.Resource; -import org.opensearch.security.spi.resources.ResourceParser; import org.opensearch.security.spi.resources.ResourceSharingExtension; +import org.opensearch.security.spi.resources.ShareableResource; +import org.opensearch.security.spi.resources.ShareableResourceParser; import org.opensearch.security.ssl.ExternalSecurityKeyStore; import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory; import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; @@ -2295,11 +2295,11 @@ public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) { String resourceType = extension.getResourceType(); String resourceIndexName = extension.getResourceIndex(); - ResourceParser resourceParser = extension.getResourceParser(); + ShareableResourceParser shareableResourceParser = extension.getShareableResourceParser(); resourceIndices.add(resourceIndexName); - ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, resourceParser); + ResourceProvider resourceProvider = new ResourceProvider(resourceType, resourceIndexName, shareableResourceParser); resourceProviders.put(resourceIndexName, resourceProvider); log.info("Loaded resource sharing extension: {}, index: {}", resourceType, resourceIndexName); } diff --git a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java index 0053e29d06..9ef51c4773 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceAccessHandler.java @@ -24,8 +24,8 @@ import org.opensearch.core.action.ActionListener; import org.opensearch.security.auth.UserSubjectImpl; import org.opensearch.security.configuration.AdminDNs; -import org.opensearch.security.spi.resources.Resource; -import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.ShareableResource; +import org.opensearch.security.spi.resources.ShareableResourceParser; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.security.spi.resources.sharing.Recipient; import org.opensearch.security.spi.resources.sharing.RecipientType; @@ -171,14 +171,14 @@ public void getAccessibleResourceIdsForCurrentUser(String resourceIndex, ActionL * @param listener The listener to be notified with the set of accessible resources. */ @SuppressWarnings("unchecked") - public void getAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { + public void getAccessibleResourcesForCurrentUser(String resourceIndex, ActionListener> listener) { try { validateArguments(resourceIndex); - ResourceParser parser = (ResourceParser) ResourcePluginInfo.getInstance() + ShareableResourceParser parser = (ShareableResourceParser) ResourcePluginInfo.getInstance() .getResourceProviders() .get(resourceIndex) - .resourceParser(); + .shareableResourceParser(); StepListener> resourceIdsListener = new StepListener<>(); StepListener> resourcesListener = new StepListener<>(); @@ -248,8 +248,10 @@ public void hasPermission(String resourceId, String resourceIndex, Set s this.resourceSharingIndexHandler.fetchDocumentById(resourceIndex, resourceId, ActionListener.wrap(document -> { if (document == null) { - LOGGER.warn("Resource '{}' not found in index '{}'", resourceId, resourceIndex); - listener.onFailure(new ResourceSharingException("Resource " + resourceId + " not found in index " + resourceIndex)); + LOGGER.warn("ShareableResource '{}' not found in index '{}'", resourceId, resourceIndex); + listener.onFailure( + new ResourceSharingException("ShareableResource " + resourceId + " not found in index " + resourceIndex) + ); return; } diff --git a/src/main/java/org/opensearch/security/resources/ResourceProvider.java b/src/main/java/org/opensearch/security/resources/ResourceProvider.java index 44936c5541..aa00375402 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceProvider.java +++ b/src/main/java/org/opensearch/security/resources/ResourceProvider.java @@ -8,8 +8,8 @@ package org.opensearch.security.resources; -import org.opensearch.security.spi.resources.Resource; -import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.ShareableResource; +import org.opensearch.security.spi.resources.ShareableResourceParser; /** * This record class represents a resource provider. @@ -17,6 +17,7 @@ * * @opensearch.experimental */ -public record ResourceProvider(String resourceType, String resourceIndexName, ResourceParser resourceParser) { +public record ResourceProvider(String resourceType, String resourceIndexName, ShareableResourceParser< + ? extends ShareableResource> shareableResourceParser) { } diff --git a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java index 54174a874a..fb52df9f1c 100644 --- a/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java +++ b/src/main/java/org/opensearch/security/resources/ResourceSharingIndexHandler.java @@ -64,8 +64,8 @@ import org.opensearch.search.SearchHit; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.security.DefaultObjectMapper; -import org.opensearch.security.spi.resources.Resource; -import org.opensearch.security.spi.resources.ResourceParser; +import org.opensearch.security.spi.resources.ShareableResource; +import org.opensearch.security.spi.resources.ShareableResourceParser; import org.opensearch.security.spi.resources.exceptions.ResourceSharingException; import org.opensearch.security.spi.resources.sharing.CreatedBy; import org.opensearch.security.spi.resources.sharing.RecipientType; @@ -413,7 +413,7 @@ public void fetchDocumentsForAllScopes( *
  • "roles" - for role-based access
  • *
  • "backend_roles" - for backend role-based access
  • * - * @param scope The scope of the access. Should be implementation of {@link org.opensearch.security.spi.resources.ResourceAccessScope} + * @param scope The scope of the access. Should be implementation of {@link org.opensearch.security} * @param listener The listener to be notified when the operation completes. * The listener receives a set of resource IDs as a result. * @throws RuntimeException if the search operation fails @@ -1158,10 +1158,10 @@ public void onFailure(Exception e) { * @param listener The listener to be notified with the set of deserialized documents. * @param The type of the deserialized documents. */ - public void getResourceDocumentsFromIds( + public void getResourceDocumentsFromIds( Set resourceIds, String resourceIndex, - ResourceParser parser, + ShareableResourceParser parser, ActionListener> listener ) { if (resourceIds.isEmpty()) { diff --git a/src/main/java/org/opensearch/security/resources/rest/ResourceAccessResponse.java b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessResponse.java index b59cee4749..0a7b15e8f9 100644 --- a/src/main/java/org/opensearch/security/resources/rest/ResourceAccessResponse.java +++ b/src/main/java/org/opensearch/security/resources/rest/ResourceAccessResponse.java @@ -17,7 +17,7 @@ import org.opensearch.core.common.io.stream.StreamOutput; import org.opensearch.core.xcontent.ToXContentObject; import org.opensearch.core.xcontent.XContentBuilder; -import org.opensearch.security.spi.resources.Resource; +import org.opensearch.security.spi.resources.ShareableResource; import org.opensearch.security.spi.resources.sharing.ResourceSharing; /** @@ -41,9 +41,9 @@ public ResourceAccessResponse(final StreamInput in) throws IOException { this.responseData = null; } - public ResourceAccessResponse(Set resources) { + public ResourceAccessResponse(Set shareableResources) { this.responseType = ResponseType.RESOURCES; - this.responseData = resources; + this.responseData = shareableResources; } public ResourceAccessResponse(ResourceSharing resourceSharing) { @@ -61,7 +61,7 @@ public ResourceAccessResponse(boolean hasPermission) { public void writeTo(StreamOutput out) throws IOException { out.writeEnum(responseType); switch (responseType) { - case RESOURCES -> out.writeCollection((Set) responseData); + case RESOURCES -> out.writeCollection((Set) responseData); case RESOURCE_SHARING -> ((ResourceSharing) responseData).writeTo(out); case BOOLEAN -> out.writeBoolean((Boolean) responseData); } @@ -79,8 +79,8 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws } @SuppressWarnings("unchecked") - public Set getResources() { - return responseType == ResponseType.RESOURCES ? (Set) responseData : Collections.emptySet(); + public Set getResources() { + return responseType == ResponseType.RESOURCES ? (Set) responseData : Collections.emptySet(); } public ResourceSharing getResourceSharing() {