diff --git a/.github/actions/create-bwc-build/action.yaml b/.github/actions/create-bwc-build/action.yaml index 8960849333..0f9e373b16 100644 --- a/.github/actions/create-bwc-build/action.yaml +++ b/.github/actions/create-bwc-build/action.yaml @@ -42,7 +42,7 @@ runs: uses: gradle/gradle-build-action@v2 with: cache-disabled: true - arguments: assemble + arguments: :assemble build-root-directory: ${{ inputs.plugin-branch }} - id: get-opensearch-version diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f9e94ad6a..4d334810b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -151,7 +151,7 @@ jobs: 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 @@ -208,7 +208,7 @@ jobs: - uses: github/codeql-action/init@v3 with: languages: java - - run: ./gradlew clean assemble + - run: ./gradlew clean :assemble - uses: github/codeql-action/analyze@v3 build-artifact-names: @@ -238,13 +238,13 @@ jobs: 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 && 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 && 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.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 :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 diff --git a/.github/workflows/plugin_install.yml b/.github/workflows/plugin_install.yml index 3f8d61795c..c427b160c4 100644 --- a/.github/workflows/plugin_install.yml +++ b/.github/workflows/plugin_install.yml @@ -32,7 +32,7 @@ jobs: uses: gradle/gradle-build-action@v3 with: cache-disabled: true - arguments: assemble + arguments: :assemble # Move and rename the plugin for installation - name: Move and rename the plugin for installation diff --git a/build.gradle b/build.gradle index 9eafa6f896..06b7aa156b 100644 --- a/build.gradle +++ b/build.gradle @@ -574,6 +574,7 @@ tasks.integrationTest.finalizedBy(jacocoTestReport) // report is always generate check.dependsOn integrationTest dependencies { + implementation project(path: ":${rootProject.name}-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}" @@ -724,29 +725,29 @@ 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.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.14.2" +// 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.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.14.2" //spotless implementation('com.google.googlejavaformat:google-java-format:1.25.2') { @@ -754,6 +755,42 @@ dependencies { } } +allprojects { + configurations { + integrationTestImplementation.extendsFrom implementation + integrationTestRuntimeOnly.extendsFrom runtimeOnly + } + dependencies { + //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.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.14.2") { + exclude(group: 'net.bytebuddy', module: 'byte-buddy') + } + integrationTestImplementation "net.bytebuddy:byte-buddy:${versions.bytebuddy}" + } +} + jar { libsDirName = '.' into '', { diff --git a/checkstyle/checkstyle.xml b/checkstyle/checkstyle.xml index a9c1a8f765..7fe4a703de 100644 --- a/checkstyle/checkstyle.xml +++ b/checkstyle/checkstyle.xml @@ -205,12 +205,12 @@ - - - - - - + + + + + + @@ -228,12 +228,12 @@ - - - - - - + + + + + + diff --git a/sample-extension-plugin/build.gradle b/sample-extension-plugin/build.gradle new file mode 100644 index 0000000000..d601890483 --- /dev/null +++ b/sample-extension-plugin/build.gradle @@ -0,0 +1,268 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +apply plugin: 'opensearch.opensearchplugin' +apply plugin: 'opensearch.testclusters' +apply plugin: 'opensearch.java-rest-test' + +import org.opensearch.gradle.test.RestIntegTestTask +import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask +import org.apache.tools.ant.taskdefs.condition.Os + +import java.util.concurrent.Callable + + +opensearchplugin { + name 'opensearch-security-sample-extension' + description 'Sample plugin that extends OpenSearch Security Resource Sharing Extension' + classname 'org.opensearch.security.sampleextension.SampleExtensionPlugin' + extendedPlugins = ['opensearch-security'] +} + +ext { + projectSubstitutions = [:] + licenseFile = rootProject.file('LICENSE.txt') + noticeFile = rootProject.file('NOTICE.txt') +} + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +//buildscript { +// ext { +// dependencies { +// classpath "org.opensearch.gradle:build-tools:${opensearch_version}" +// } +// } +//} + +configurations { + all { + resolutionStrategy { + force 'org.slf4j:slf4j-api:1.7.36' + force 'commons-codec:commons-codec:1.17.1' + force "org.apache.httpcomponents:httpclient:4.5.14" + force "org.apache.httpcomponents:httpcore:4.4.16" + force "com.carrotsearch.randomizedtesting:randomizedtesting-runner:2.8.1" + force "org.hamcrest:hamcrest:2.2" + force "com.fasterxml.jackson:jackson-bom:${versions.jackson}" + force "com.fasterxml.jackson.core:jackson-core:${versions.jackson}" + force "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:${versions.jackson}" + force "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + } + } +} + +// TODO Make this plugin have an integration test dependency on integrationTest from root project +dependencies { + compileOnly project(path: ":${rootProject.name}-spi", configuration: 'shadow') + // integrationTestImplementation project(":") + integrationTestImplementation rootProject.sourceSets.main.output + integrationTestImplementation rootProject.sourceSets.integrationTest.output + testImplementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" + testImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + + integrationTestImplementation "org.opensearch.plugin:transport-netty4-client:${opensearch_version}" + integrationTestImplementation "org.opensearch.client:opensearch-rest-high-level-client:${opensearch_version}" + integrationTestImplementation "com.google.guava:guava:${guava_version}" + integrationTestImplementation 'org.greenrobot:eventbus-java:3.3.1' + integrationTestImplementation 'org.ldaptive:ldaptive:1.2.3' + integrationTestImplementation 'com.password4j:password4j:1.8.2' + // Action privileges: check tables and compact collections + integrationTestImplementation 'com.selectivem.collections:special-collections-complete:1.4.0' + // JSON patch + integrationTestImplementation 'com.flipkart.zjsonpatch:zjsonpatch:0.4.16' + //Password generation + integrationTestImplementation 'org.passay:passay:1.6.5' + integrationTestImplementation 'org.slf4j:slf4j-api:1.7.36' + integrationTestImplementation "org.apache.commons:commons-lang3:${versions.commonslang}" + + integrationTestImplementation "com.fasterxml.jackson.core:jackson-annotations:${versions.jackson}" + integrationTestImplementation "com.fasterxml.jackson.core:jackson-databind:${versions.jackson_databind}" + + integrationTestImplementation "org.opensearch:opensearch:${opensearch_version}" +} + +def es_tmp_dir = rootProject.file('build/private/es_tmp').absoluteFile +es_tmp_dir.mkdirs() + +File repo = file("$buildDir/testclusters/repo") +def _numNodes = findProperty('numNodes') as Integer ?: 1 + +licenseHeaders.enabled = true +validateNebulaPom.enabled = false +testingConventions.enabled = false +loggerUsageCheck.enabled = false + +javaRestTest.dependsOn(rootProject.assemble) +javaRestTest { + systemProperty 'tests.security.manager', 'false' +} +testClusters.javaRestTest { + testDistribution = 'INTEG_TEST' +} + +task integTest(type: RestIntegTestTask) { + description = "Run tests against a cluster" + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath +} +tasks.named("check").configure { dependsOn(integTest) } + +//create source set 'integrationTest' +//add classes from the main source set to the compilation and runtime classpaths of the integrationTest +sourceSets { + integrationTest { + java { + srcDir file ('src/integrationTest/java') + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + } + resources { + srcDir file('src/integrationTest/resources') + } + processIntegrationTestResources { + duplicatesStrategy(DuplicatesStrategy.INCLUDE) + } + } +} + +task integrationTest(type: Test) { + testClassesDirs = sourceSets.integrationTest.output.classesDirs + classpath = sourceSets.integrationTest.runtimeClasspath +} + +configurations { + integrationTestImplementation.extendsFrom(rootProject.configurations.implementation) + integrationTestImplementation.extendsFrom(rootProject.configurations.testImplementation) + integrationTestImplementation.extendsFrom(rootProject.configurations.integrationTestImplementation) +} + +integTest { + if (project.hasProperty('excludeTests')) { + project.properties['excludeTests']?.replaceAll('\\s', '')?.split('[,;]')?.each { + exclude "${it}" + } + } + systemProperty 'tests.security.manager', 'false' + systemProperty 'java.io.tmpdir', es_tmp_dir.absolutePath + + systemProperty "https", System.getProperty("https") + systemProperty "user", System.getProperty("user") + systemProperty "password", System.getProperty("password") + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can use longer timeouts for + // requests. The 'doFirst' delays reading the debug setting on the cluster till execution time. + doFirst { + // Tell the test JVM if the cluster JVM is running under a debugger so that tests can + // use longer timeouts for requests. + def isDebuggingCluster = getDebug() || System.getProperty("test.debug") != null + systemProperty 'cluster.debug', isDebuggingCluster + // Set number of nodes system property to be used in tests + systemProperty 'cluster.number_of_nodes', "${_numNodes}" + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + + // The -Dcluster.debug option makes the cluster debuggable; this makes the tests debuggable + if (System.getProperty("test.debug") != null) { + jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000' + } + if (System.getProperty("tests.rest.bwcsuite") == null) { + filter { + excludeTestsMatching "org.opensearch.security.sampleextension.bwc.*IT" + } + } +} +project.getTasks().getByName('bundlePlugin').dependsOn(rootProject.tasks.getByName('build')) +Zip bundle = (Zip) project.getTasks().getByName("bundlePlugin"); +Zip rootBundle = (Zip) rootProject.getTasks().getByName("bundlePlugin"); +integTest.dependsOn(bundle) +integTest.getClusters().forEach{c -> { + c.plugin(rootProject.getObjects().fileProperty().value(rootBundle.getArchiveFile())) + c.plugin(project.getObjects().fileProperty().value(bundle.getArchiveFile())) +}} + +testClusters.integTest { + testDistribution = 'INTEG_TEST' + + // Cluster shrink exception thrown if we try to set numberOfNodes to 1, so only apply if > 1 + if (_numNodes > 1) numberOfNodes = _numNodes + // When running integration tests it doesn't forward the --debug-jvm to the cluster anymore + // i.e. we have to use a custom property to flag when we want to debug OpenSearch JVM + // since we also support multi node integration tests we increase debugPort per node + if (System.getProperty("cluster.debug") != null) { + def debugPort = 5005 + nodes.forEach { node -> + node.jvmArgs("-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=*:${debugPort}") + debugPort += 1 + } + } + setting 'path.repo', repo.absolutePath +} + +afterEvaluate { + testClusters.integTest.nodes.each { node -> + def plugins = node.plugins + def firstPlugin = plugins.get(0) + if (firstPlugin.provider == project.bundlePlugin.archiveFile) { + plugins.remove(0) + plugins.add(firstPlugin) + } + + node.extraConfigFile("kirk.pem", file("src/test/resources/security/kirk.pem")) + node.extraConfigFile("kirk-key.pem", file("src/test/resources/security/kirk-key.pem")) + node.extraConfigFile("esnode.pem", file("src/test/resources/security/esnode.pem")) + node.extraConfigFile("esnode-key.pem", file("src/test/resources/security/esnode-key.pem")) + node.extraConfigFile("root-ca.pem", file("src/test/resources/security/root-ca.pem")) + node.setting("plugins.security.ssl.transport.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.transport.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.transport.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.ssl.transport.enforce_hostname_verification", "false") + node.setting("plugins.security.ssl.http.enabled", "true") + node.setting("plugins.security.ssl.http.pemcert_filepath", "esnode.pem") + node.setting("plugins.security.ssl.http.pemkey_filepath", "esnode-key.pem") + node.setting("plugins.security.ssl.http.pemtrustedcas_filepath", "root-ca.pem") + node.setting("plugins.security.allow_unsafe_democertificates", "true") + node.setting("plugins.security.allow_default_init_securityindex", "true") + node.setting("plugins.security.authcz.admin_dn", "\n - CN=kirk,OU=client,O=client,L=test,C=de") + node.setting("plugins.security.audit.type", "internal_opensearch") + node.setting("plugins.security.enable_snapshot_restore_privilege", "true") + node.setting("plugins.security.check_snapshot_restore_write_privileges", "true") + node.setting("plugins.security.restapi.roles_enabled", "[\"all_access\", \"security_rest_api_access\"]") + } +} + +run { + doFirst { + // There seems to be an issue when running multi node run or integ tasks with unicast_hosts + // not being written, the waitForAllConditions ensures it's written + getClusters().forEach { cluster -> + cluster.waitForAllConditions() + } + } + useCluster testClusters.integTest +} + +// As of ES 7.7 the sample-extension-plugin is being added to the list of plugins for the testCluster during build before +// the security plugin is causing build failures. +// The security zip is added explicitly above but the sample-extension-plugin is added implicitly at some time during evaluation. +// Will need to do a deep dive to find out exactly what task adds the sample-extension-plugin and add security there but a temporary hack is to +// reorder the plugins list after evaluation but prior to task execution when the plugins are installed. +//afterEvaluate { +// testClusters.javaRestTest.nodes.each { node -> +// def nodePlugins = node.plugins +// def firstPlugin = nodePlugins.get(0) +// if (firstPlugin.provider == project.bundlePlugin.archiveFile) { +// nodePlugins.remove(0) +// nodePlugins.add(firstPlugin) +// } +// } +//} diff --git a/sample-extension-plugin/src/integrationTest/java/org/opensearch/security/sampleextension/SampleExtensionPluginTests.java b/sample-extension-plugin/src/integrationTest/java/org/opensearch/security/sampleextension/SampleExtensionPluginTests.java new file mode 100644 index 0000000000..8d22e01410 --- /dev/null +++ b/sample-extension-plugin/src/integrationTest/java/org/opensearch/security/sampleextension/SampleExtensionPluginTests.java @@ -0,0 +1,95 @@ +/* + * 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.sampleextension; + +import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; +import org.apache.http.HttpStatus; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.opensearch.test.framework.cluster.ClusterManager; +import org.opensearch.test.framework.cluster.LocalCluster; +import org.opensearch.test.framework.cluster.TestRestClient; +import org.opensearch.test.framework.cluster.TestRestClient.HttpResponse; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.opensearch.test.framework.TestSecurityConfig.AuthcDomain.AUTHC_HTTPBASIC_INTERNAL; +import static org.opensearch.test.framework.TestSecurityConfig.User.USER_ADMIN; + +@RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) +@ThreadLeakScope(ThreadLeakScope.Scope.NONE) +public class SampleExtensionPluginTests { + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .plugin(SampleExtensionPlugin.class) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN) + .build(); + + @Test + public void testSecurityRoles() throws Exception { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(HttpStatus.SC_OK); + + // Check username + assertThat(response.getTextFromJsonBody("/user_name"), equalTo("admin")); + System.out.println("Response: " + response.getBody()); + HttpResponse pluginsResponse = client.get("_cat/plugins?s=component&h=name,component,version,description"); + System.out.println("pluginsResponse: " + pluginsResponse.getBody()); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.OpenSearchSecurityPlugin")); + assertThat(pluginsResponse.getBody(), containsString("org.opensearch.security.sampleextension.SampleExtensionPlugin")); + } + } + + @Test + public void testCreateAndUpdateOwnSampleResource() throws Exception { + String resourceId; + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + String sampleResource = "{\"name\":\"sample\"}"; + HttpResponse response = client.postJson("_plugins/resource_sharing_example/resource", sampleResource); + response.assertStatusCode(HttpStatus.SC_OK); + System.out.println("Response: " + response.getBody()); + + resourceId = response.getTextFromJsonBody("/resourceId"); + + System.out.println("resourceId: " + resourceId); + Thread.sleep(2000); + } + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse response = client.postJson(".resource-sharing/_search", "{\"query\" : {\"match_all\" : {}}}"); + System.out.println("Resource sharing entries: " + response.getBody()); + } + + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Thread.sleep(1000); + + HttpResponse getResponse = client.get("_plugins/resource_sharing_example/resource/" + resourceId); + getResponse.assertStatusCode(HttpStatus.SC_OK); + System.out.println("Get Response: " + getResponse.getBody()); + + String sampleResourceUpdated = "{\"name\":\"sampleUpdated\"}"; + HttpResponse updateResponse = client.putJson( + "_plugins/resource_sharing_example/resource/update/" + resourceId, + sampleResourceUpdated + ); + updateResponse.assertStatusCode(HttpStatus.SC_OK); + System.out.println("Update Response: " + updateResponse.getBody()); + } + } + +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/SampleExtensionPlugin.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/SampleExtensionPlugin.java new file mode 100644 index 0000000000..6431446329 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/SampleExtensionPlugin.java @@ -0,0 +1,150 @@ +/* + * 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.sampleextension; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.ActionRequest; +import org.opensearch.client.Client; +import org.opensearch.cluster.metadata.IndexNameExpressionResolver; +import org.opensearch.cluster.node.DiscoveryNodes; +import org.opensearch.cluster.service.ClusterService; +import org.opensearch.common.settings.ClusterSettings; +import org.opensearch.common.settings.IndexScopedSettings; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.settings.SettingsFilter; +import org.opensearch.core.action.ActionResponse; +import org.opensearch.core.common.io.stream.NamedWriteableRegistry; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.env.Environment; +import org.opensearch.env.NodeEnvironment; +import org.opensearch.indices.SystemIndexDescriptor; +import org.opensearch.plugins.ActionPlugin; +import org.opensearch.plugins.Plugin; +import org.opensearch.plugins.SystemIndexPlugin; +import org.opensearch.repositories.RepositoriesService; +import org.opensearch.rest.RestController; +import org.opensearch.rest.RestHandler; +import org.opensearch.script.ScriptService; +import org.opensearch.security.sampleextension.actions.create.CreateSampleResourceAction; +import org.opensearch.security.sampleextension.actions.create.CreateSampleResourceRestAction; +import org.opensearch.security.sampleextension.actions.create.CreateSampleResourceTransportAction; +import org.opensearch.security.sampleextension.actions.get.GetSampleResourceAction; +import org.opensearch.security.sampleextension.actions.get.GetSampleResourceRestAction; +import org.opensearch.security.sampleextension.actions.get.GetSampleResourceTransportAction; +import org.opensearch.security.sampleextension.actions.list.ListSampleResourceAction; +import org.opensearch.security.sampleextension.actions.list.ListSampleResourceRestAction; +import org.opensearch.security.sampleextension.actions.list.ListSampleResourceTransportAction; +import org.opensearch.security.sampleextension.actions.update.UpdateSampleResourceAction; +import org.opensearch.security.sampleextension.actions.update.UpdateSampleResourceRestAction; +import org.opensearch.security.sampleextension.actions.update.UpdateSampleResourceTransportAction; +import org.opensearch.security.sampleextension.resource.SampleResourceParser; +import org.opensearch.security.sampleextension.resource.SampleResourceSharingServiceProvider; +import org.opensearch.security.spi.ResourceParser; +import org.opensearch.security.spi.ResourceSharingExtension; +import org.opensearch.security.spi.ResourceSharingService; +import org.opensearch.security.spi.SharableResource; +import org.opensearch.threadpool.ThreadPool; +import org.opensearch.watcher.ResourceWatcherService; + +/** + * Sample Security Resource Sharing extension plugin. + * + * It use ".sample_extension_resources" index to manage its resources, and exposes a REST API + * + */ +public class SampleExtensionPlugin extends Plugin implements ActionPlugin, SystemIndexPlugin, ResourceSharingExtension { + private static final Logger log = LogManager.getLogger(SampleExtensionPlugin.class); + + public static final String RESOURCE_INDEX_NAME = ".sample_extension_resources"; + + private Client client; + + @Override + public Collection createComponents( + Client client, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { + this.client = client; + System.out.println( + "SampleResourceSharingServiceProvider.getInstance(): " + SampleResourceSharingServiceProvider.getInstance().get() + ); + return List.of(SampleResourceSharingServiceProvider.getInstance()); + } + + @Override + public List getRestHandlers( + Settings settings, + RestController restController, + ClusterSettings clusterSettings, + IndexScopedSettings indexScopedSettings, + SettingsFilter settingsFilter, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier nodesInCluster + ) { + return List.of( + new CreateSampleResourceRestAction(), + new GetSampleResourceRestAction(), + new ListSampleResourceRestAction(), + new UpdateSampleResourceRestAction() + ); + } + + @Override + public List> getActions() { + return List.of( + new ActionHandler<>(CreateSampleResourceAction.INSTANCE, CreateSampleResourceTransportAction.class), + new ActionHandler<>(GetSampleResourceAction.INSTANCE, GetSampleResourceTransportAction.class), + new ActionHandler<>(ListSampleResourceAction.INSTANCE, ListSampleResourceTransportAction.class), + new ActionHandler<>(UpdateSampleResourceAction.INSTANCE, UpdateSampleResourceTransportAction.class) + ); + } + + @Override + public Collection getSystemIndexDescriptors(Settings settings) { + final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(RESOURCE_INDEX_NAME, "Example index with resources"); + return Collections.singletonList(systemIndexDescriptor); + } + + @Override + public String getResourceType() { + return "sample_resource"; + } + + @Override + public String getResourceIndex() { + return RESOURCE_INDEX_NAME; + } + + @Override + public ResourceParser getResourceParser() { + return new SampleResourceParser(); + } + + @SuppressWarnings("unchecked") + @Override + public void assignResourceSharingService(ResourceSharingService service) { + SampleResourceSharingServiceProvider.getInstance().set(service); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceAction.java new file mode 100644 index 0000000000..618c31f24c --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceAction.java @@ -0,0 +1,30 @@ +/* + * 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.sampleextension.actions.create; + +import org.opensearch.action.ActionType; +import org.opensearch.security.spi.actions.resource.create.CreateResourceResponse; + +/** + * Action to create a sample resource + */ +public class CreateSampleResourceAction extends ActionType { + /** + * Create sample resource action instance + */ + public static final CreateSampleResourceAction INSTANCE = new CreateSampleResourceAction(); + /** + * Create sample resource action name + */ + public static final String NAME = "cluster:admin/sampleresource/create"; + + private CreateSampleResourceAction() { + super(NAME, CreateResourceResponse::new); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceRestAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceRestAction.java new file mode 100644 index 0000000000..315619b1d3 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceRestAction.java @@ -0,0 +1,57 @@ +/* + * 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.sampleextension.actions.create; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.sampleextension.resource.SampleResource; +import org.opensearch.security.spi.actions.resource.create.CreateResourceRequest; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.POST; + +public class CreateSampleResourceRestAction extends BaseRestHandler { + + public CreateSampleResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(POST, "/_plugins/resource_sharing_example/resource")); + } + + @Override + public String getName() { + return "create_sample_resource"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String name = (String) source.get("name"); + SampleResource resource = new SampleResource(); + resource.setName(name); + final CreateResourceRequest createSampleResourceRequest = new CreateResourceRequest<>(resource); + return channel -> client.executeLocally( + CreateSampleResourceAction.INSTANCE, + createSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceTransportAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceTransportAction.java new file mode 100644 index 0000000000..a98f22c3f8 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/create/CreateSampleResourceTransportAction.java @@ -0,0 +1,33 @@ +/* + * 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.sampleextension.actions.create; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.security.sampleextension.resource.SampleResource; +import org.opensearch.security.spi.actions.resource.create.CreateResourceTransportAction; +import org.opensearch.transport.TransportService; + +import static org.opensearch.security.sampleextension.SampleExtensionPlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for CreateSampleResource. + */ +public class CreateSampleResourceTransportAction extends CreateResourceTransportAction { + private static final Logger log = LogManager.getLogger(CreateSampleResourceTransportAction.class); + + @Inject + public CreateSampleResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(transportService, actionFilters, nodeClient, CreateSampleResourceAction.NAME, RESOURCE_INDEX_NAME, SampleResource::new); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceAction.java new file mode 100644 index 0000000000..889b9f51b7 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceAction.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.sampleextension.actions.get; + +import org.opensearch.action.ActionType; +import org.opensearch.security.sampleextension.resource.SampleResource; +import org.opensearch.security.spi.actions.resource.get.GetResourceResponse; + +/** + * Action to get a sample resource + */ +public class GetSampleResourceAction extends ActionType> { + /** + * Get sample resource action instance + */ + public static final GetSampleResourceAction INSTANCE = new GetSampleResourceAction(); + /** + * Get sample resource action name + */ + public static final String NAME = "cluster:admin/sampleresource/get"; + + private GetSampleResourceAction() { + super(NAME, in -> new GetResourceResponse<>(in, SampleResource::new)); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceRestAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceRestAction.java new file mode 100644 index 0000000000..bb517d7668 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceRestAction.java @@ -0,0 +1,50 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.sampleextension.actions.get; + +import java.io.IOException; +import java.util.List; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.spi.actions.resource.get.GetResourceRequest; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.security.sampleextension.SampleExtensionPlugin.RESOURCE_INDEX_NAME; + +public class GetSampleResourceRestAction extends BaseRestHandler { + + public GetSampleResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, "/_plugins/resource_sharing_example/resource/{id}")); + } + + @Override + public String getName() { + return "get_sample_resource"; + } + + @SuppressWarnings("unchecked") + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + String resourceId = request.param("id"); + + final GetResourceRequest getSampleResourceRequest = new GetResourceRequest(resourceId, RESOURCE_INDEX_NAME); + return channel -> client.executeLocally( + GetSampleResourceAction.INSTANCE, + getSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceTransportAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceTransportAction.java new file mode 100644 index 0000000000..12d470abf5 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/get/GetSampleResourceTransportAction.java @@ -0,0 +1,51 @@ +/* + * 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.sampleextension.actions.get; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.sampleextension.resource.SampleResource; +import org.opensearch.security.sampleextension.resource.SampleResourceParser; +import org.opensearch.security.sampleextension.resource.SampleResourceSharingServiceProvider; +import org.opensearch.security.spi.actions.resource.get.GetResourceTransportAction; +import org.opensearch.transport.TransportService; + +import static org.opensearch.security.sampleextension.SampleExtensionPlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for GetSampleResource. + */ +public class GetSampleResourceTransportAction extends GetResourceTransportAction { + private static final Logger log = LogManager.getLogger(GetSampleResourceTransportAction.class); + + @Inject + public GetSampleResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + SampleResourceSharingServiceProvider resourceSharingService, + Client client, + NamedXContentRegistry xContentRegistry + ) { + super( + transportService, + actionFilters, + GetSampleResourceAction.NAME, + RESOURCE_INDEX_NAME, + resourceSharingService.get(), + new SampleResourceParser(), + client, + xContentRegistry + ); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceAction.java new file mode 100644 index 0000000000..0da69f169b --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceAction.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.sampleextension.actions.list; + +import org.opensearch.action.ActionType; +import org.opensearch.security.sampleextension.resource.SampleResource; +import org.opensearch.security.spi.actions.resource.list.ListResourceResponse; + +/** + * Action to list sample resources + */ +public class ListSampleResourceAction extends ActionType> { + /** + * List sample resource action instance + */ + public static final ListSampleResourceAction INSTANCE = new ListSampleResourceAction(); + /** + * List sample resource action name + */ + public static final String NAME = "cluster:admin/sampleresource/list"; + + private ListSampleResourceAction() { + super(NAME, in -> new ListResourceResponse<>(in, SampleResource::new)); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceRestAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceRestAction.java new file mode 100644 index 0000000000..da30b498f5 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceRestAction.java @@ -0,0 +1,46 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.sampleextension.actions.list; + +import java.util.List; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.spi.actions.resource.list.ListResourceRequest; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.GET; +import static org.opensearch.security.sampleextension.SampleExtensionPlugin.RESOURCE_INDEX_NAME; + +public class ListSampleResourceRestAction extends BaseRestHandler { + + public ListSampleResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(GET, "/_plugins/resource_sharing_example/resource")); + } + + @Override + public String getName() { + return "list_sample_resources"; + } + + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) { + final ListResourceRequest listSampleResourceRequest = new ListResourceRequest(RESOURCE_INDEX_NAME); + return channel -> client.executeLocally( + ListSampleResourceAction.INSTANCE, + listSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceTransportAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceTransportAction.java new file mode 100644 index 0000000000..821a26a2b7 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/list/ListSampleResourceTransportAction.java @@ -0,0 +1,51 @@ +/* + * 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.sampleextension.actions.list; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.support.ActionFilters; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.security.sampleextension.resource.SampleResource; +import org.opensearch.security.sampleextension.resource.SampleResourceParser; +import org.opensearch.security.sampleextension.resource.SampleResourceSharingServiceProvider; +import org.opensearch.security.spi.actions.resource.list.ListResourceTransportAction; +import org.opensearch.transport.TransportService; + +import static org.opensearch.security.sampleextension.SampleExtensionPlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for ListSampleResource. + */ +public class ListSampleResourceTransportAction extends ListResourceTransportAction { + private static final Logger log = LogManager.getLogger(ListSampleResourceTransportAction.class); + + @Inject + public ListSampleResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + SampleResourceSharingServiceProvider resourceSharingService, + NamedXContentRegistry xContentRegistry, + Client client + ) { + super( + transportService, + actionFilters, + ListSampleResourceAction.NAME, + RESOURCE_INDEX_NAME, + resourceSharingService.get(), + new SampleResourceParser(), + client, + xContentRegistry + ); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceAction.java new file mode 100644 index 0000000000..cb1a6a394c --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceAction.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.sampleextension.actions.update; + +import org.opensearch.action.ActionType; + +/** + * Action to update a sample resource + */ +public class UpdateSampleResourceAction extends ActionType { + /** + * Update sample resource action instance + */ + public static final UpdateSampleResourceAction INSTANCE = new UpdateSampleResourceAction(); + /** + * Update sample resource action name + */ + public static final String NAME = "cluster:admin/sampleresource/update"; + + private UpdateSampleResourceAction() { + super(NAME, UpdateSampleResourceResponse::new); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceRequest.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceRequest.java new file mode 100644 index 0000000000..1f35dcffd6 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceRequest.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.sampleextension.actions.update; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for UpdateSampleResource transport action + */ +public class UpdateSampleResourceRequest extends ActionRequest { + + private String resourceId; + private String name; + + public UpdateSampleResourceRequest(String resourceId, String name) { + this.resourceId = resourceId; + this.name = name; + } + + public String getResourceId() { + return resourceId; + } + + public String getName() { + return name; + } + + /** + * Constructor with stream input + * @param in the stream input + * @throws IOException IOException + */ + public UpdateSampleResourceRequest(final StreamInput in) throws IOException {} + + @Override + public void writeTo(final StreamOutput out) throws IOException {} + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceResponse.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceResponse.java new file mode 100644 index 0000000000..83babbc847 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceResponse.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.sampleextension.actions.update; + +import java.io.IOException; + +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; + +/** + * Response to a UpdateSampleResourceRequest + */ +public class UpdateSampleResourceResponse extends ActionResponse implements ToXContentObject { + private final String resourceId; + + /** + * Default constructor + * + * @param resourceId The resourceId + */ + public UpdateSampleResourceResponse(String resourceId) { + this.resourceId = resourceId; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(resourceId); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public UpdateSampleResourceResponse(final StreamInput in) throws IOException { + resourceId = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resourceId", resourceId); + builder.endObject(); + return builder; + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceRestAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceRestAction.java new file mode 100644 index 0000000000..8658193c9b --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceRestAction.java @@ -0,0 +1,57 @@ +/* + * 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.sampleextension.actions.update; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; + +import static java.util.Collections.singletonList; +import static org.opensearch.rest.RestRequest.Method.PUT; + +public class UpdateSampleResourceRestAction extends BaseRestHandler { + + public UpdateSampleResourceRestAction() {} + + @Override + public List routes() { + return singletonList(new Route(PUT, "/_plugins/resource_sharing_example/resource/update/{id}")); + } + + @Override + public String getName() { + return "update_sample_resource"; + } + + @SuppressWarnings("unchecked") + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + String resourceId = request.param("id"); + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + String name = (String) source.get("name"); + + // TODO Update the request obj + final UpdateSampleResourceRequest updateSampleResourceRequest = new UpdateSampleResourceRequest(resourceId, name); + return channel -> client.executeLocally( + UpdateSampleResourceAction.INSTANCE, + updateSampleResourceRequest, + new RestToXContentListener<>(channel) + ); + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceTransportAction.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceTransportAction.java new file mode 100644 index 0000000000..9070622371 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/actions/update/UpdateSampleResourceTransportAction.java @@ -0,0 +1,74 @@ +/* + * 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.sampleextension.actions.update; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.security.sampleextension.resource.SampleResource; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.opensearch.security.sampleextension.SampleExtensionPlugin.RESOURCE_INDEX_NAME; + +/** + * Transport action for UpdateSampleResource. + */ +public class UpdateSampleResourceTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(UpdateSampleResourceTransportAction.class); + + private final Client nodeClient; + + @Inject + public UpdateSampleResourceTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(UpdateSampleResourceAction.NAME, transportService, actionFilters, UpdateSampleResourceRequest::new); + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, UpdateSampleResourceRequest request, ActionListener actionListener) { + indexResource(request, actionListener); + } + + private void indexResource(UpdateSampleResourceRequest request, ActionListener listener) { + log.warn("resourceId: " + request.getResourceId()); + String name = request.getName(); + SampleResource updatedResource = new SampleResource(); + updatedResource.setName(name); + try { + IndexRequest ir = nodeClient.prepareIndex(RESOURCE_INDEX_NAME) + .setId(request.getResourceId()) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(updatedResource.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .request(); + + log.warn("Index Request: " + ir.toString()); + + ActionListener irListener = ActionListener.wrap(idxResponse -> { + log.info("Updated resource: " + idxResponse.toString()); + listener.onResponse(new UpdateSampleResourceResponse(idxResponse.getId())); + }, listener::onFailure); + nodeClient.index(ir, irListener); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResource.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResource.java new file mode 100644 index 0000000000..6d3ca30e56 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResource.java @@ -0,0 +1,60 @@ +package org.opensearch.security.sampleextension.resource; + +import java.io.IOException; +import java.time.Instant; + +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.security.spi.SharableResource; + +public class SampleResource implements SharableResource { + + private String name; + private Instant lastUpdateTime; + + public SampleResource() { + Instant now = Instant.now(); + this.lastUpdateTime = now; + } + + public SampleResource(StreamInput in) throws IOException { + this.name = in.readString(); + this.lastUpdateTime = in.readInstant(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field("name", name).endObject(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeInstant(lastUpdateTime); + + } + + @Override + public String getWriteableName() { + return "sample_resource"; + } + + @Override + public String getName() { + return name; + } + + @Override + public Instant getLastUpdateTime() { + return lastUpdateTime; + } + + public void setName(String name) { + this.name = name; + } + + public void setLastUpdateTime(Instant lastUpdateTime) { + this.lastUpdateTime = lastUpdateTime; + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResourceParser.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResourceParser.java new file mode 100644 index 0000000000..6ecd4e2e73 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResourceParser.java @@ -0,0 +1,29 @@ +package org.opensearch.security.sampleextension.resource; + +import java.io.IOException; + +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.core.xcontent.XContentParserUtils; +import org.opensearch.security.spi.ResourceParser; + +public class SampleResourceParser implements ResourceParser { + + @Override + public SampleResource parse(XContentParser parser, String id) throws IOException { + SampleResource resource = new SampleResource(); + XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + + while (!parser.nextToken().equals(XContentParser.Token.END_OBJECT)) { + String fieldName = parser.currentName(); + parser.nextToken(); + switch (fieldName) { + case "name": + resource.setName(parser.text()); + break; + default: + XContentParserUtils.throwUnknownToken(parser.currentToken(), parser.getTokenLocation()); + } + } + return resource; + } +} diff --git a/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResourceSharingServiceProvider.java b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResourceSharingServiceProvider.java new file mode 100644 index 0000000000..92a1939274 --- /dev/null +++ b/sample-extension-plugin/src/main/java/org/opensearch/security/sampleextension/resource/SampleResourceSharingServiceProvider.java @@ -0,0 +1,62 @@ +package org.opensearch.security.sampleextension.resource; + +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.opensearch.common.inject.Provider; +import org.opensearch.security.spi.ResourceSharingService; + +/** + * Provider for ResourceSharingService that handles SampleResource instances. + * This provider allows for flexible injection of different ResourceSharingService + * implementations based on runtime conditions. + */ +public final class SampleResourceSharingServiceProvider implements Provider { + + private volatile ResourceSharingService resourceSharingService; + + private static final Map instances = new ConcurrentHashMap<>(); + + private SampleResourceSharingServiceProvider() {} + + @SuppressWarnings("removal") + public static SampleResourceSharingServiceProvider getInstance() { + ClassLoader classLoader = AccessController.doPrivileged( + (PrivilegedAction) () -> Thread.currentThread().getContextClassLoader() + ); + instances.computeIfAbsent(classLoader, cl -> new SampleResourceSharingServiceProvider()); + return instances.get(classLoader); + } + + /** + * Sets the resource sharing service implementation. + * This method is thread-safe and ensures the service is only set once. + * + * @param resourceSharingService the service implementation to use + * @throws IllegalStateException if the service has already been set + * @throws IllegalArgumentException if the provided service is null + */ + public void set(ResourceSharingService resourceSharingService) { + if (resourceSharingService == null) { + throw new IllegalArgumentException("ResourceSharingService cannot be null"); + } + + if (this.resourceSharingService != null) { + throw new IllegalStateException("ResourceSharingService has already been set"); + } + + this.resourceSharingService = resourceSharingService; + } + + /** + * {@inheritDoc} + * + * @return the configured ResourceSharingService + */ + @Override + public ResourceSharingService get() { + return resourceSharingService; + } +} diff --git a/sample-extension-plugin/src/main/plugin-metadata/plugin-security.policy b/sample-extension-plugin/src/main/plugin-metadata/plugin-security.policy new file mode 100644 index 0000000000..197a86c647 --- /dev/null +++ b/sample-extension-plugin/src/main/plugin-metadata/plugin-security.policy @@ -0,0 +1,4 @@ +grant { + permission java.lang.RuntimePermission "getClassLoader"; + permission java.lang.RuntimePermission "accessDeclaredMembers"; +}; \ No newline at end of file diff --git a/sample-extension-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.ResourceSharingExtension b/sample-extension-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.ResourceSharingExtension new file mode 100644 index 0000000000..e32f06ee56 --- /dev/null +++ b/sample-extension-plugin/src/main/resources/META-INF/services/org.opensearch.security.spi.ResourceSharingExtension @@ -0,0 +1,6 @@ +# +# Copyright OpenSearch Contributors +# SPDX-License-Identifier: Apache-2.0 +# + +org.opensearch.security.sampleextension.SampleExtensionPlugin \ No newline at end of file diff --git a/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/ODFERestTestCase.java b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/ODFERestTestCase.java new file mode 100644 index 0000000000..39a3b97810 --- /dev/null +++ b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/ODFERestTestCase.java @@ -0,0 +1,207 @@ +/* + * 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.sampleextension; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.net.ssl.SSLEngine; + +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.NoopHostnameVerifier; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.message.BasicHeader; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.ssl.TlsDetails; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.util.Timeout; +import org.junit.After; + +import org.opensearch.client.Request; +import org.opensearch.client.Response; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.client.WarningFailureException; +import org.opensearch.common.io.PathUtils; +import org.opensearch.common.settings.Settings; +import org.opensearch.common.unit.TimeValue; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.xcontent.DeprecationHandler; +import org.opensearch.core.xcontent.MediaType; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.test.rest.OpenSearchRestTestCase; + +public abstract class ODFERestTestCase extends OpenSearchRestTestCase { + + protected boolean isHttps() { + boolean isHttps = Optional.ofNullable(System.getProperty("https")).map("true"::equalsIgnoreCase).orElse(false); + if (isHttps) { + // currently only external cluster is supported for security enabled testing + if (!Optional.ofNullable(System.getProperty("tests.rest.cluster")).isPresent()) { + throw new RuntimeException("cluster url should be provided for security enabled testing"); + } + } + + return isHttps; + } + + @Override + protected String getProtocol() { + return isHttps() ? "https" : "http"; + } + + @Override + protected Settings restAdminSettings() { + return Settings.builder() + .put("http.port", 9200) + .put("plugins.security.ssl.http.enabled", isHttps()) + .put("plugins.security.ssl.http.pemcert_filepath", "sample.pem") + .put("plugins.security.ssl.http.keystore_filepath", "test-kirk.jks") + .put("plugins.security.ssl.http.keystore_password", "changeit") + .build(); + // return Settings.builder().put("strictDeprecationMode", false).put("http.port", 9200).build(); + } + + @Override + protected RestClient buildClient(Settings settings, HttpHost[] hosts) throws IOException { + boolean strictDeprecationMode = settings.getAsBoolean("strictDeprecationMode", true); + RestClientBuilder builder = RestClient.builder(hosts); + if (isHttps()) { + String keystore = settings.get("plugins.security.ssl.http.keystore_filepath"); + if (Objects.nonNull(keystore)) { + URI uri = null; + try { + uri = this.getClass().getClassLoader().getResource("security/sample.pem").toURI(); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + Path configPath = PathUtils.get(uri).getParent().toAbsolutePath(); + return new SecureRestClientBuilder(settings, configPath, hosts).build(); + } else { + configureHttpsClient(builder, settings); + builder.setStrictDeprecationMode(strictDeprecationMode); + return builder.build(); + } + } else { + configureClient(builder, settings); + builder.setStrictDeprecationMode(strictDeprecationMode); + return builder.build(); + } + + } + + @SuppressWarnings("unchecked") + @After + protected void wipeAllODFEIndices() throws IOException { + Response response = adminClient().performRequest(new Request("GET", "/_cat/indices?format=json&expand_wildcards=all")); + MediaType mediaType = MediaType.fromMediaType(response.getEntity().getContentType()); + try ( + XContentParser parser = mediaType.xContent() + .createParser( + NamedXContentRegistry.EMPTY, + DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + response.getEntity().getContent() + ) + ) { + XContentParser.Token token = parser.nextToken(); + List> parserList = null; + if (token == XContentParser.Token.START_ARRAY) { + parserList = parser.listOrderedMap().stream().map(obj -> (Map) obj).collect(Collectors.toList()); + } else { + parserList = Collections.singletonList(parser.mapOrdered()); + } + + for (Map index : parserList) { + String indexName = (String) index.get("index"); + if (indexName != null && !".opendistro_security".equals(indexName)) { + try { + adminClient().performRequest(new Request("DELETE", "/" + indexName)); + } catch (WarningFailureException ignore) {} + } + } + } + } + + protected static void configureHttpsClient(RestClientBuilder builder, Settings settings) throws IOException { + Map headers = new HashMap<>(ThreadContext.buildDefaultHeaders(settings)); + String userName = Optional.ofNullable(System.getProperty("user")).orElseThrow(() -> new RuntimeException("user name is missing")); + String password = Optional.ofNullable(System.getProperty("password")) + .orElseThrow(() -> new RuntimeException("password is missing")); + headers.put( + "Authorization", + "Basic " + Base64.getEncoder().encodeToString((userName + ":" + password).getBytes(StandardCharsets.UTF_8)) + ); + headers.put("Content-Type", "application/json"); + Header[] defaultHeaders = new Header[headers.size()]; + int i = 0; + for (Map.Entry entry : headers.entrySet()) { + defaultHeaders[i++] = new BasicHeader(entry.getKey(), entry.getValue()); + } + builder.setDefaultHeaders(defaultHeaders); + builder.setHttpClientConfigCallback(httpClientBuilder -> { + try { + final TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() + .setSslContext(SSLContextBuilder.create().loadTrustMaterial(null, (chains, authType) -> true).build()) + // disable the certificate since our testing cluster just uses the default security configuration + .setHostnameVerifier(NoopHostnameVerifier.INSTANCE) + // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 + .setTlsDetailsFactory(new Factory() { + @Override + public TlsDetails create(final SSLEngine sslEngine) { + return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); + } + }) + .build(); + + final PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .build(); + + return httpClientBuilder.setConnectionManager(connectionManager); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + final String socketTimeoutString = settings.get(CLIENT_SOCKET_TIMEOUT); + final TimeValue socketTimeout = TimeValue.parseTimeValue( + socketTimeoutString == null ? "60s" : socketTimeoutString, + CLIENT_SOCKET_TIMEOUT + ); + builder.setRequestConfigCallback( + conf -> conf.setResponseTimeout(Timeout.ofMilliseconds(Math.toIntExact(socketTimeout.getMillis()))) + ); + if (settings.hasValue(CLIENT_PATH_PREFIX)) { + builder.setPathPrefix(settings.get(CLIENT_PATH_PREFIX)); + } + } + + /** + * wipeAllIndices won't work since it cannot delete security index. Use wipeAllODFEIndices instead. + */ + @Override + protected boolean preserveIndicesUponCompletion() { + return true; + } +} diff --git a/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/SampleExtensionPluginIT.java b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/SampleExtensionPluginIT.java new file mode 100644 index 0000000000..6bb3a88e56 --- /dev/null +++ b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/SampleExtensionPluginIT.java @@ -0,0 +1,155 @@ +/* + * 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.sampleextension; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.junit.Assert; + +import org.opensearch.client.Request; +import org.opensearch.client.RequestOptions; +import org.opensearch.client.Response; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.json.JsonXContent; +import org.opensearch.core.xcontent.NamedXContentRegistry; + +public class SampleExtensionPluginIT extends ODFERestTestCase { + + // @BeforeClass + // public static void createTestUsers() throws IOException { + // Request createUserRequest = new Request("POST", "/_opendistro/_security/api/internalusers/craig"); + // createUserRequest.setJsonEntity("{\"password\":\"changeme\",\"roles\":[\"all_access\"]}"); + // client().performRequest(createUserRequest); + // } + + @SuppressWarnings("unchecked") + public void testPluginsAreInstalled() throws IOException { + Request request = new Request("GET", "/_cat/plugins?s=component&h=name,component,version,description&format=json"); + Response response = client().performRequest(request); + List pluginsList = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + response.getEntity().getContent() + ).list(); + Assert.assertTrue( + pluginsList.stream().map(o -> (Map) o).anyMatch(plugin -> plugin.get("component").equals("opensearch-security")) + ); + Assert.assertTrue( + pluginsList.stream() + .map(o -> (Map) o) + .anyMatch(plugin -> plugin.get("component").equals("opensearch-security-sample-extension")) + ); + } + + public void testCreateSampleResource() throws IOException, InterruptedException { + String strongPassword = "myStrongPassword123!"; + Request createUserRequest = new Request("PUT", "/_opendistro/_security/api/internalusers/craig"); + createUserRequest.setJsonEntity("{\"password\":\"" + strongPassword + "\",\"backend_roles\":[\"admin\"]}"); + client().performRequest(createUserRequest); + + RequestOptions.Builder requestOptions = RequestOptions.DEFAULT.toBuilder(); + requestOptions.setWarningsHandler((warnings) -> false); + + Request createRequest = new Request("POST", "/_plugins/resource_sharing_example/resource"); + createRequest.setEntity(new StringEntity("{\"name\":\"ExampleResource1\"}")); + createRequest.setOptions(requestOptions); + Response response = client().performRequest(createRequest); + Map createResourceResponse = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + response.getEntity().getContent() + ).mapStrings(); + System.out.println("createResourceResponse: " + createResourceResponse); + + Request createRequest2 = new Request("POST", "/_plugins/resource_sharing_example/resource"); + createRequest2.setEntity(new StringEntity("{\"name\":\"ExampleResource2\"}")); + RequestOptions.Builder requestOptions2 = RequestOptions.DEFAULT.toBuilder(); + requestOptions2.setWarningsHandler((warnings) -> false); + requestOptions2.addHeader( + "Authorization", + "Basic " + Base64.getEncoder().encodeToString(("craig:" + strongPassword).getBytes(StandardCharsets.UTF_8)) + ); + createRequest2.setOptions(requestOptions2); + Response response2 = client().performRequest(createRequest2); + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode indexResponseNode = objectMapper.readTree(response2.getEntity().getContent()); + + String resourceId = indexResponseNode.get("resourceId").asText(); + Map createResourceResponse2 = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + response2.getEntity().getContent() + ).mapStrings(); + System.out.println("createResourceResponse2: " + createResourceResponse2); + + // Sleep to give ResourceSharingListener time to create the .resource-sharing index + Thread.sleep(1000); + + Request listRequest = new Request("GET", "/_plugins/resource_sharing_example/resource"); + listRequest.setOptions(requestOptions); + Response listResponse = client().performRequest(listRequest); + JsonNode resNode = objectMapper.readTree(listResponse.getEntity().getContent()); + System.out.println("resNode: " + resNode); + Map listResourceResponse = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + listResponse.getEntity().getContent() + ).map(); + System.out.println("listResourceResponse: " + listResourceResponse); + + Request resourceSharingRequest = new Request("POST", "/.resource-sharing/_search"); + resourceSharingRequest.setOptions(requestOptions); + Response resourceSharingResponse = adminClient().performRequest(resourceSharingRequest); + Map resourceSharingResponseMap = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + resourceSharingResponse.getEntity().getContent() + ).map(); + System.out.println("resourceSharingResponse: " + resourceSharingResponseMap); + + Request updateSharingRequest = new Request("PUT", "/_plugins/_security/resource/sample_resource/" + resourceId + "/share_with"); + updateSharingRequest.setEntity( + new StringEntity("{\"share_with\":{\"users\": [\"admin\"], \"backend_roles\": [], \"allowed_actions\": [\"*\"]}}") + ); + requestOptions.addHeader("Content-Type", "application/json"); + updateSharingRequest.setOptions(requestOptions); + Response updateResponse = client().performRequest(updateSharingRequest); + Map updateSharingResponse = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + updateResponse.getEntity().getContent() + ).mapStrings(); + System.out.println("updateSharingResponse: " + updateSharingResponse); + + Thread.sleep(1000); + + resourceSharingResponse = adminClient().performRequest(resourceSharingRequest); + resourceSharingResponseMap = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + resourceSharingResponse.getEntity().getContent() + ).map(); + System.out.println("resourceSharingResponse after update: " + resourceSharingResponseMap); + + listResponse = client().performRequest(listRequest); + listResourceResponse = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, + LoggingDeprecationHandler.INSTANCE, + listResponse.getEntity().getContent() + ).map(); + System.out.println("listResourceResponse after update: " + listResourceResponse); + } +} diff --git a/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/SecureRestClientBuilder.java b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/SecureRestClientBuilder.java new file mode 100644 index 0000000000..6a0a6d33e1 --- /dev/null +++ b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/SecureRestClientBuilder.java @@ -0,0 +1,236 @@ +/* + * 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.sampleextension; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.LinkOption; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.Arrays; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +import org.apache.hc.client5.http.auth.AuthScope; +import org.apache.hc.client5.http.auth.CredentialsProvider; +import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.impl.async.HttpAsyncClientBuilder; +import org.apache.hc.client5.http.impl.auth.BasicCredentialsProvider; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManager; +import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder; +import org.apache.hc.client5.http.ssl.ClientTlsStrategyBuilder; +import org.apache.hc.client5.http.ssl.TrustSelfSignedStrategy; +import org.apache.hc.core5.function.Factory; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.nio.ssl.TlsStrategy; +import org.apache.hc.core5.reactor.ssl.TlsDetails; +import org.apache.hc.core5.ssl.SSLContextBuilder; +import org.apache.hc.core5.util.Timeout; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.client.RestClient; +import org.opensearch.client.RestClientBuilder; +import org.opensearch.common.settings.Settings; +import org.opensearch.core.common.Strings; + +/** + * Provides builder to create low-level and high-level REST client to make calls to OpenSearch. + * + * Sample usage: + * SecureRestClientBuilder builder = new SecureRestClientBuilder(settings).build() + * RestClient restClient = builder.build(); + * + * Other usage: + * RestClient restClient = new SecureRestClientBuilder("localhost", 9200, false) + * .setUserPassword("admin", "myStrongPassword123") + * .setTrustCerts(trustStorePath) + * .build(); + * + * + * If https is enabled, creates RestClientBuilder using self-signed certificates or passed pem + * as trusted. + * + * If https is not enabled, creates a http based client. + */ +public class SecureRestClientBuilder { + + private final boolean httpSSLEnabled; + private final String user; + private final String passwd; + private final ArrayList hosts = new ArrayList<>(); + + private final Path configPath; + private final Settings settings; + + private int defaultConnectTimeOutMSecs = 5000; + private int defaultSoTimeoutMSecs = 10000; + private int defaultConnRequestTimeoutMSecs = 3 * 60 * 1000; /* 3 mins */ + private int defaultMaxConnPerRoute = RestClientBuilder.DEFAULT_MAX_CONN_PER_ROUTE; + private int defaultMaxConnTotal = RestClientBuilder.DEFAULT_MAX_CONN_TOTAL; + + private static final Logger log = LogManager.getLogger(SecureRestClientBuilder.class); + + public SecureRestClientBuilder(Settings settings, Path configPath, HttpHost[] httpHosts) { + this.httpSSLEnabled = settings.getAsBoolean("plugins.security.ssl.http.enabled", false); + this.settings = settings; + this.configPath = configPath; + this.user = null; + this.passwd = null; + hosts.addAll(Arrays.asList(httpHosts)); + } + + /** + * Creates a low-level Rest client. + * @return + * @throws IOException + */ + public RestClient build() throws IOException { + return createRestClientBuilder().build(); + } + + private RestClientBuilder createRestClientBuilder() throws IOException { + RestClientBuilder builder = RestClient.builder(hosts.toArray(new HttpHost[hosts.size()])); + + builder.setRequestConfigCallback(new RestClientBuilder.RequestConfigCallback() { + @Override + public RequestConfig.Builder customizeRequestConfig(RequestConfig.Builder requestConfigBuilder) { + return requestConfigBuilder.setConnectTimeout(Timeout.ofMilliseconds(defaultConnectTimeOutMSecs)) + .setResponseTimeout(Timeout.ofMilliseconds(defaultSoTimeoutMSecs)) + .setConnectionRequestTimeout(Timeout.ofMilliseconds(defaultConnRequestTimeoutMSecs)); + } + }); + + final SSLContext sslContext; + try { + sslContext = createSSLContext(); + } catch (GeneralSecurityException | IOException ex) { + throw new IOException(ex); + } + final CredentialsProvider credentialsProvider = createCredsProvider(); + builder.setHttpClientConfigCallback(new RestClientBuilder.HttpClientConfigCallback() { + @Override + public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpClientBuilder) { + if (sslContext != null) { + TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() + .setSslContext(sslContext) + // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 + .setTlsDetailsFactory(new Factory() { + @Override + public TlsDetails create(final SSLEngine sslEngine) { + return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); + } + }) + .build(); + PoolingAsyncClientConnectionManager connectionManager = PoolingAsyncClientConnectionManagerBuilder.create() + .setTlsStrategy(tlsStrategy) + .setMaxConnPerRoute(defaultMaxConnPerRoute) + .setMaxConnTotal(defaultMaxConnTotal) + .build(); + httpClientBuilder.setConnectionManager(connectionManager); + } + if (credentialsProvider != null) { + httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); + } + return httpClientBuilder; + } + }); + return builder; + } + + private SSLContext createSSLContext() throws IOException, GeneralSecurityException { + SSLContextBuilder builder = new SSLContextBuilder(); + if (httpSSLEnabled) { + // Handle trust store + String pemFile = getTrustPem(); + if (Strings.isNullOrEmpty(pemFile)) { + builder.loadTrustMaterial(null, new TrustSelfSignedStrategy()); + } else { + String pem = resolve(pemFile, configPath); + KeyStore trustStore = new TrustStore(pem).create(); + builder.loadTrustMaterial(trustStore, null); + } + + // Handle key store. + KeyStore keyStore = getKeyStore(); + if (keyStore != null) { + builder.loadKeyMaterial(keyStore, getKeystorePasswd().toCharArray()); + } + + } + return builder.build(); + } + + private CredentialsProvider createCredsProvider() { + if (Strings.isNullOrEmpty(user) || Strings.isNullOrEmpty(passwd)) return null; + + final BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials(new AuthScope(null, -1), new UsernamePasswordCredentials(user, passwd.toCharArray())); + return credentialsProvider; + } + + private String resolve(final String originalFile, final Path configPath) { + String path = null; + if (originalFile != null && originalFile.length() > 0) { + path = configPath.resolve(originalFile).toAbsolutePath().toString(); + log.debug("Resolved {} to {} against {}", originalFile, path, configPath.toAbsolutePath().toString()); + } + + if (path == null || path.length() == 0) { + throw new OpenSearchException("Empty file path for " + originalFile); + } + + if (Files.isDirectory(Paths.get(path), LinkOption.NOFOLLOW_LINKS)) { + throw new OpenSearchException("Is a directory: " + path + " Expected a file for " + originalFile); + } + + if (!Files.isReadable(Paths.get(path))) { + throw new OpenSearchException( + "Unable to read " + + path + + " (" + + Paths.get(path) + + "). Please make sure this files exists and is readable regarding to permissions. Property: " + + originalFile + ); + } + if ("".equals(path)) { + path = null; + } + return path; + } + + private String getTrustPem() { + return settings.get("plugins.security.ssl.http.pemcert_filepath", null); + } + + private String getKeystorePasswd() { + return settings.get("plugins.security.ssl.http.keystore_password", null); + } + + private KeyStore getKeyStore() throws IOException, GeneralSecurityException { + KeyStore keyStore = KeyStore.getInstance("jks"); + String keyStoreFile = settings.get("plugins.security.ssl.http.keystore_filepath", null); + String passwd = settings.get("plugins.security.ssl.http.keystore_password", null); + if (Strings.isNullOrEmpty(keyStoreFile) || Strings.isNullOrEmpty(passwd)) { + return null; + } + String keyStorePath = resolve(keyStoreFile, configPath); + try (InputStream is = Files.newInputStream(Paths.get(keyStorePath))) { + keyStore.load(is, passwd.toCharArray()); + } + return keyStore; + } +} diff --git a/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/TrustStore.java b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/TrustStore.java new file mode 100644 index 0000000000..2bee6291d1 --- /dev/null +++ b/sample-extension-plugin/src/test/java/org/opensearch/security/sampleextension/TrustStore.java @@ -0,0 +1,71 @@ +/* + * 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.sampleextension; + +import java.io.FileInputStream; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Collection; + +/** + * Helper class to read raw pem files to keystore. + */ +public class TrustStore { + + private final String effectiveKeyAlias = "al"; + private final String storeType = "JKS"; + private final String certType = "X.509"; + private final String cert; + + public TrustStore(final String file) { + cert = file; + } + + public KeyStore create() throws IOException, GeneralSecurityException { + X509Certificate[] trustCerts = loadCertificatesFromFile(cert); + return toTrustStore(effectiveKeyAlias, trustCerts); + } + + private X509Certificate[] loadCertificatesFromFile(String file) throws IOException, GeneralSecurityException { + if (file == null) { + return null; + } + CertificateFactory fact = CertificateFactory.getInstance(certType); + try (FileInputStream is = new FileInputStream(file)) { + Collection certs = fact.generateCertificates(is); + X509Certificate[] x509Certs = new X509Certificate[certs.size()]; + int i = 0; + for (Certificate cert : certs) { + x509Certs[i++] = (X509Certificate) cert; + } + return x509Certs; + } + } + + private KeyStore toTrustStore(final String trustCertificatesAliasPrefix, final X509Certificate[] trustCertificates) throws IOException, + GeneralSecurityException { + if (trustCertificates == null) { + return null; + } + KeyStore ks = KeyStore.getInstance(storeType); + ks.load(null); + + if (trustCertificates != null) { + for (int i = 0; i < trustCertificates.length; i++) { + X509Certificate x509Certificate = trustCertificates[i]; + ks.setCertificateEntry(trustCertificatesAliasPrefix + "_" + i, x509Certificate); + } + } + return ks; + } +} diff --git a/sample-extension-plugin/src/test/resources/security/esnode-key.pem b/sample-extension-plugin/src/test/resources/security/esnode-key.pem new file mode 100644 index 0000000000..e90562be43 --- /dev/null +++ b/sample-extension-plugin/src/test/resources/security/esnode-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCm93kXteDQHMAv +bUPNPW5pyRHKDD42XGWSgq0k1D29C/UdyL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0 +o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0HGkn47XVu3EwbfrTENg3jFu+Oem6a/50 +1SzITzJWtS0cn2dIFOBimTVpT/4Zv5qrXA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1 +MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8ndibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b +6l+KLo3IKpfTbAIJXIO+M67FLtWKtttDao94B069skzKk6FPgW/OZh6PRCD0oxOa +vV+ld2SjAgMBAAECggEAQK1+uAOZeaSZggW2jQut+MaN4JHLi61RH2cFgU3COLgo +FIiNjFn8f2KKU3gpkt1It8PjlmprpYut4wHI7r6UQfuv7ZrmncRiPWHm9PB82+ZQ +5MXYqj4YUxoQJ62Cyz4sM6BobZDrjG6HHGTzuwiKvHHkbsEE9jQ4E5m7yfbVvM0O +zvwrSOM1tkZihKSTpR0j2+taji914tjBssbn12TMZQL5ItGnhR3luY8mEwT9MNkZ +xg0VcREoAH+pu9FE0vPUgLVzhJ3be7qZTTSRqv08bmW+y1plu80GbppePcgYhEow +dlW4l6XPJaHVSn1lSFHE6QAx6sqiAnBz0NoTPIaLyQKBgQDZqDOlhCRciMRicSXn +7yid9rhEmdMkySJHTVFOidFWwlBcp0fGxxn8UNSBcXdSy7GLlUtH41W9PWl8tp9U +hQiiXORxOJ7ZcB80uNKXF01hpPj2DpFPWyHFxpDkWiTAYpZl68rOlYujxZUjJIej +VvcykBC2BlEOG9uZv2kxcqLyJwKBgQDEYULTxaTuLIa17wU3nAhaainKB3vHxw9B +Ksy5p3ND43UNEKkQm7K/WENx0q47TA1mKD9i+BhaLod98mu0YZ+BCUNgWKcBHK8c +uXpauvM/pLhFLXZ2jvEJVpFY3J79FSRK8bwE9RgKfVKMMgEk4zOyZowS8WScOqiy +hnQn1vKTJQKBgElhYuAnl9a2qXcC7KOwRsJS3rcKIVxijzL4xzOyVShp5IwIPbOv +hnxBiBOH/JGmaNpFYBcBdvORE9JfA4KMQ2fx53agfzWRjoPI1/7mdUk5RFI4gRb/ +A3jZRBoopgFSe6ArCbnyQxzYzToG48/Wzwp19ZxYrtUR4UyJct6f5n27AoGBAJDh +KIpQQDOvCdtjcbfrF4aM2DPCfaGPzENJriwxy6oEPzDaX8Bu/dqI5Ykt43i/zQrX +GpyLaHvv4+oZVTiI5UIvcVO9U8hQPyiz9f7F+fu0LHZs6f7hyhYXlbe3XFxeop3f +5dTKdWgXuTTRF2L9dABkA2deS9mutRKwezWBMQk5AoGBALPtX0FrT1zIosibmlud +tu49A/0KZu4PBjrFMYTSEWGNJez3Fb2VsJwylVl6HivwbP61FhlYfyksCzQQFU71 ++x7Nmybp7PmpEBECr3deoZKQ/acNHn0iwb0It+YqV5+TquQebqgwK6WCLsMuiYKT +bg/ch9Rhxbq22yrVgWHh6epp +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-extension-plugin/src/test/resources/security/esnode.pem b/sample-extension-plugin/src/test/resources/security/esnode.pem new file mode 100644 index 0000000000..44101f0b37 --- /dev/null +++ b/sample-extension-plugin/src/test/resources/security/esnode.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-extension-plugin/src/test/resources/security/kirk-key.pem b/sample-extension-plugin/src/test/resources/security/kirk-key.pem new file mode 100644 index 0000000000..1949c26139 --- /dev/null +++ b/sample-extension-plugin/src/test/resources/security/kirk-key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCVXDgEJQorgfXp +gpY0TgF55bD2xuzxN5Dc9rDfgWxrsOvOloMpd7k6FR71bKWjJi1KptSmM/cDElky +AWYKSfYWGiGxsQ+EQW+6kwCfEOHXQldn+0+JcWqP+osSPjtJfwRvRN5kRqP69MPo +7U0N2kdqenqMWjmG1chDGLRSOEGU5HIBiDxsZtOcvMaJ8b1eaW0lvS+6gFQ80AvB +GBkDDCOHHLtDXBylrZk2CQP8AzxNicIZ4B8G3CG3OHA8+nBtEtxZoIihrrkqlMt+ +b/5N8u8zB0Encew0kdrc4R/2wS//ahr6U+9Siq8T7WsUtGwKj3BJClg6OyDJRhlu +y2gFnxoPAgMBAAECggEAP5TOycDkx+megAWVoHV2fmgvgZXkBrlzQwUG/VZQi7V4 +ZGzBMBVltdqI38wc5MtbK3TCgHANnnKgor9iq02Z4wXDwytPIiti/ycV9CDRKvv0 +TnD2hllQFjN/IUh5n4thHWbRTxmdM7cfcNgX3aZGkYbLBVVhOMtn4VwyYu/Mxy8j +xClZT2xKOHkxqwmWPmdDTbAeZIbSv7RkIGfrKuQyUGUaWhrPslvYzFkYZ0umaDgQ +OAthZew5Bz3OfUGOMPLH61SVPuJZh9zN1hTWOvT65WFWfsPd2yStI+WD/5PU1Doo +1RyeHJO7s3ug8JPbtNJmaJwHe9nXBb/HXFdqb976yQKBgQDNYhpu+MYSYupaYqjs +9YFmHQNKpNZqgZ4ceRFZ6cMJoqpI5dpEMqToFH7tpor72Lturct2U9nc2WR0HeEs +/6tiptyMPTFEiMFb1opQlXF2ae7LeJllntDGN0Q6vxKnQV+7VMcXA0Y8F7tvGDy3 +qJu5lfvB1mNM2I6y/eMxjBuQhwKBgQC6K41DXMFro0UnoO879pOQYMydCErJRmjG +/tZSy3Wj4KA/QJsDSViwGfvdPuHZRaG9WtxdL6kn0w1exM9Rb0bBKl36lvi7o7xv +M+Lw9eyXMkww8/F5d7YYH77gIhGo+RITkKI3+5BxeBaUnrGvmHrpmpgRXWmINqr0 +0jsnN3u0OQKBgCf45vIgItSjQb8zonLz2SpZjTFy4XQ7I92gxnq8X0Q5z3B+o7tQ +K/4rNwTju/sGFHyXAJlX+nfcK4vZ4OBUJjP+C8CTjEotX4yTNbo3S6zjMyGQqDI5 +9aIOUY4pb+TzeUFJX7If5gR+DfGyQubvvtcg1K3GHu9u2l8FwLj87sRzAoGAflQF +RHuRiG+/AngTPnZAhc0Zq0kwLkpH2Rid6IrFZhGLy8AUL/O6aa0IGoaMDLpSWUJp +nBY2S57MSM11/MVslrEgGmYNnI4r1K25xlaqV6K6ztEJv6n69327MS4NG8L/gCU5 +3pEm38hkUi8pVYU7in7rx4TCkrq94OkzWJYurAkCgYATQCL/rJLQAlJIGulp8s6h +mQGwy8vIqMjAdHGLrCS35sVYBXG13knS52LJHvbVee39AbD5/LlWvjJGlQMzCLrw +F7oILW5kXxhb8S73GWcuMbuQMFVHFONbZAZgn+C9FW4l7XyRdkrbR1MRZ2km8YMs +/AHmo368d4PSNRMMzLHw8Q== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/sample-extension-plugin/src/test/resources/security/kirk.pem b/sample-extension-plugin/src/test/resources/security/kirk.pem new file mode 100644 index 0000000000..36b7e19a75 --- /dev/null +++ b/sample-extension-plugin/src/test/resources/security/kirk.pem @@ -0,0 +1,27 @@ +-----BEGIN CERTIFICATE----- +MIIEmDCCA4CgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLcwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzA0MjRaFw0zNDAyMTcxNzA0MjRaME0xCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ8wDQYDVQQKDAZjbGllbnQxDzANBgNVBAsMBmNs +aWVudDENMAsGA1UEAwwEa2lyazCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAJVcOAQlCiuB9emCljROAXnlsPbG7PE3kNz2sN+BbGuw686Wgyl3uToVHvVs +paMmLUqm1KYz9wMSWTIBZgpJ9hYaIbGxD4RBb7qTAJ8Q4ddCV2f7T4lxao/6ixI+ +O0l/BG9E3mRGo/r0w+jtTQ3aR2p6eoxaOYbVyEMYtFI4QZTkcgGIPGxm05y8xonx +vV5pbSW9L7qAVDzQC8EYGQMMI4ccu0NcHKWtmTYJA/wDPE2JwhngHwbcIbc4cDz6 +cG0S3FmgiKGuuSqUy35v/k3y7zMHQSdx7DSR2tzhH/bBL/9qGvpT71KKrxPtaxS0 +bAqPcEkKWDo7IMlGGW7LaAWfGg8CAwEAAaOCASswggEnMAwGA1UdEwEB/wQCMAAw +DgYDVR0PAQH/BAQDAgXgMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMB0GA1UdDgQW +BBSjMS8tgguX/V7KSGLoGg7K6XMzIDCBzwYDVR0jBIHHMIHEgBQXh9+gWutmEqfV +0Pi6EkU8tysAnKGBlaSBkjCBjzETMBEGCgmSJomT8ixkARkWA2NvbTEXMBUGCgmS +JomT8ixkARkWB2V4YW1wbGUxGTAXBgNVBAoMEEV4YW1wbGUgQ29tIEluYy4xITAf +BgNVBAsMGEV4YW1wbGUgQ29tIEluYy4gUm9vdCBDQTEhMB8GA1UEAwwYRXhhbXBs +ZSBDb20gSW5jLiBSb290IENBghQNZAmZZn3EFOxBR4630XlhI+mo4jANBgkqhkiG +9w0BAQsFAAOCAQEACEUPPE66/Ot3vZqRGpjDjPHAdtOq+ebaglQhvYcnDw8LOZm8 +Gbh9M88CiO6UxC8ipQLTPh2yyeWArkpJzJK/Pi1eoF1XLiAa0sQ/RaJfQWPm9dvl +1ZQeK5vfD4147b3iBobwEV+CR04SKow0YeEEzAJvzr8YdKI6jqr+2GjjVqzxvRBy +KRVHWCFiR7bZhHGLq3br8hSu0hwjb3oGa1ZI8dui6ujyZt6nm6BoEkau3G/6+zq9 +E6vX3+8Fj4HKCAL6i0SwfGmEpTNp5WUhqibK/fMhhmMT4Mx6MxkT+OFnIjdUU0S/ +e3kgnG8qjficUr38CyEli1U0M7koIXUZI7r+LQ== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-extension-plugin/src/test/resources/security/root-ca.pem b/sample-extension-plugin/src/test/resources/security/root-ca.pem new file mode 100644 index 0000000000..d33f5f7216 --- /dev/null +++ b/sample-extension-plugin/src/test/resources/security/root-ca.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIExjCCA66gAwIBAgIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAwMzZaFw0zNDAyMTcxNzAwMzZaMIGPMRMwEQYKCZIm +iZPyLGQBGRYDY29tMRcwFQYKCZImiZPyLGQBGRYHZXhhbXBsZTEZMBcGA1UECgwQ +RXhhbXBsZSBDb20gSW5jLjEhMB8GA1UECwwYRXhhbXBsZSBDb20gSW5jLiBSb290 +IENBMSEwHwYDVQQDDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEPyN7J9VGPyJcQmCBl5TGwfSzvVdWwoQU +j9aEsdfFJ6pBCDQSsj8Lv4RqL0dZra7h7SpZLLX/YZcnjikrYC+rP5OwsI9xEE/4 +U98CsTBPhIMgqFK6SzNE5494BsAk4cL72dOOc8tX19oDS/PvBULbNkthQ0aAF1dg +vbrHvu7hq7LisB5ZRGHVE1k/AbCs2PaaKkn2jCw/b+U0Ml9qPuuEgz2mAqJDGYoA +WSR4YXrOcrmPuRqbws464YZbJW898/0Pn/U300ed+4YHiNYLLJp51AMkR4YEw969 +VRPbWIvLrd0PQBooC/eLrL6rvud/GpYhdQEUx8qcNCKd4bz3OaQ5AgMBAAGjggEW +MIIBEjAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQU +F4ffoFrrZhKn1dD4uhJFPLcrAJwwgc8GA1UdIwSBxzCBxIAUF4ffoFrrZhKn1dD4 +uhJFPLcrAJyhgZWkgZIwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJ +k/IsZAEZFgdleGFtcGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYD +VQQLDBhFeGFtcGxlIENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUg +Q29tIEluYy4gUm9vdCBDQYIUDWQJmWZ9xBTsQUeOt9F5YSPpqOIwDQYJKoZIhvcN +AQELBQADggEBAL3Q3AHUhMiLUy6OlLSt8wX9I2oNGDKbBu0atpUNDztk/0s3YLQC +YuXgN4KrIcMXQIuAXCx407c+pIlT/T1FNn+VQXwi56PYzxQKtlpoKUL3oPQE1d0V +6EoiNk+6UodvyZqpdQu7fXVentRMk1QX7D9otmiiNuX+GSxJhJC2Lyzw65O9EUgG +1yVJon6RkUGtqBqKIuLksKwEr//ELnjmXit4LQKSnqKr0FTCB7seIrKJNyb35Qnq +qy9a/Unhokrmdda1tr6MbqU8l7HmxLuSd/Ky+L0eDNtYv6YfMewtjg0TtAnFyQov +rdXmeq1dy9HLo3Ds4AFz3Gx9076TxcRS/iI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-extension-plugin/src/test/resources/security/sample.pem b/sample-extension-plugin/src/test/resources/security/sample.pem new file mode 100644 index 0000000000..44101f0b37 --- /dev/null +++ b/sample-extension-plugin/src/test/resources/security/sample.pem @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEPDCCAySgAwIBAgIUaYSlET3nzsotWTrWueVPPh10yLYwDQYJKoZIhvcNAQEL +BQAwgY8xEzARBgoJkiaJk/IsZAEZFgNjb20xFzAVBgoJkiaJk/IsZAEZFgdleGFt +cGxlMRkwFwYDVQQKDBBFeGFtcGxlIENvbSBJbmMuMSEwHwYDVQQLDBhFeGFtcGxl +IENvbSBJbmMuIFJvb3QgQ0ExITAfBgNVBAMMGEV4YW1wbGUgQ29tIEluYy4gUm9v +dCBDQTAeFw0yNDAyMjAxNzAzMjVaFw0zNDAyMTcxNzAzMjVaMFcxCzAJBgNVBAYT +AmRlMQ0wCwYDVQQHDAR0ZXN0MQ0wCwYDVQQKDARub2RlMQ0wCwYDVQQLDARub2Rl +MRswGQYDVQQDDBJub2RlLTAuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUA +A4IBDwAwggEKAoIBAQCm93kXteDQHMAvbUPNPW5pyRHKDD42XGWSgq0k1D29C/Ud +yL21HLzTJa49ZU2ldIkSKs9JqbkHdyK0o8MO6L8dotLoYbxDWbJFW8bp1w6tDTU0 +HGkn47XVu3EwbfrTENg3jFu+Oem6a/501SzITzJWtS0cn2dIFOBimTVpT/4Zv5qr +XA6Cp4biOmoTYWhi/qQl8d0IaADiqoZ1MvZbZ6x76qTrRAbg+UWkpTEXoH1xTc8n +dibR7+HP6OTqCKvo1NhE8uP4pY+fWd6b6l+KLo3IKpfTbAIJXIO+M67FLtWKtttD +ao94B069skzKk6FPgW/OZh6PRCD0oxOavV+ld2SjAgMBAAGjgcYwgcMwRwYDVR0R +BEAwPogFKgMEBQWCEm5vZGUtMC5leGFtcGxlLmNvbYIJbG9jYWxob3N0hxAAAAAA +AAAAAAAAAAAAAAABhwR/AAABMAsGA1UdDwQEAwIF4DAdBgNVHSUEFjAUBggrBgEF +BQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU0/qDQaY10jIo +wCjLUpz/HfQXyt8wHwYDVR0jBBgwFoAUF4ffoFrrZhKn1dD4uhJFPLcrAJwwDQYJ +KoZIhvcNAQELBQADggEBAGbij5WyF0dKhQodQfTiFDb73ygU6IyeJkFSnxF67gDz +pQJZKFvXuVBa3cGP5e7Qp3TK50N+blXGH0xXeIV9lXeYUk4hVfBlp9LclZGX8tGi +7Xa2enMvIt5q/Yg3Hh755ZxnDYxCoGkNOXUmnMusKstE0YzvZ5Gv6fcRKFBUgZLh +hUBqIEAYly1EqH/y45APiRt3Nor1yF6zEI4TnL0yNrHw6LyQkUNCHIGMJLfnJQ9L +camMGIXOx60kXNMTigF9oXXwixWAnDM9y3QT8QXA7hej/4zkbO+vIeV/7lGUdkyg +PAi92EvyxmsliEMyMR0VINl8emyobvfwa7oMeWMR+hg= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/sample-extension-plugin/src/test/resources/security/test-kirk.jks b/sample-extension-plugin/src/test/resources/security/test-kirk.jks new file mode 100644 index 0000000000..6c8c5ef77e Binary files /dev/null and b/sample-extension-plugin/src/test/resources/security/test-kirk.jks differ diff --git a/settings.gradle b/settings.gradle index 1c3e7ff5aa..407e70f319 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,9 @@ */ rootProject.name = 'opensearch-security' + +include "spi" +project(":spi").name = rootProject.name + "-spi" + +include "sample-extension-plugin" +project(":sample-extension-plugin").name = rootProject.name + "-sample-extension" diff --git a/spi/build.gradle b/spi/build.gradle new file mode 100644 index 0000000000..a0613e5226 --- /dev/null +++ b/spi/build.gradle @@ -0,0 +1,150 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin +import org.opensearch.gradle.test.RestIntegTestTask + +plugins { + id 'com.github.johnrengelman.shadow' + id 'jacoco' + id 'maven-publish' + id 'signing' + id "org.gradle.test-retry" version "1.6.0" +} + +apply plugin: 'opensearch.java' +apply plugin: 'opensearch.testclusters' +apply plugin: 'opensearch.java-rest-test' + +repositories { + mavenLocal() + mavenCentral() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } +} + +ext { + projectSubstitutions = [:] + licenseFile = rootProject.file('LICENSE.txt') + noticeFile = rootProject.file('NOTICE') +} + +jacoco { + toolVersion = '0.8.7' + reportsDirectory = file("$buildDir/JacocoReport") +} + +jacocoTestReport { + reports { + xml.required = false + csv.required = false + html.destination file("${buildDir}/jacoco/") + } +} +check.dependsOn jacocoTestReport + +dependencies { + compileOnly "org.opensearch:opensearch:${opensearch_version}" + testImplementation "org.opensearch.test:framework:${opensearch_version}" + testImplementation "org.apache.logging.log4j:log4j-core:${versions.log4j}" +} + +configurations.all { + if (it.state != Configuration.State.UNRESOLVED) return +} + +shadowJar { + archiveClassifier = null +} + +test { + retry { + failOnPassedAfterRetry = false + maxRetries = 5 + } + doFirst { + // reverse operation of https://github.com/elastic/elasticsearch/blob/7.6/buildSrc/src/main/groovy/org/elasticsearch/gradle/BuildPlugin.groovy#L736-L743 + // to fix the classpath for unit tests + test.classpath -= project.files(project.tasks.named('shadowJar')) + test.classpath -= project.configurations.getByName(ShadowBasePlugin.CONFIGURATION_NAME) + test.classpath += project.extensions.getByType(SourceSetContainer).getByName(SourceSet.MAIN_SOURCE_SET_NAME).runtimeClasspath + } + // add "-Dtests.security.manager=false" to VM options if you want to run integ tests in IntelliJ + systemProperty 'tests.security.manager', 'false' +} + +task integTest(type: RestIntegTestTask) { + description 'Run integ test with opensearch test framework' + group 'verification' + systemProperty 'tests.security.manager', 'false' + dependsOn test +} +check.dependsOn integTest + +testClusters.javaRestTest { + testDistribution = 'INTEG_TEST' +} + +task sourcesJar(type: Jar) { + archiveClassifier.set 'sources' + from sourceSets.main.allJava +} + +task javadocJar(type: Jar) { + archiveClassifier.set 'javadoc' + from javadoc.destinationDir + dependsOn javadoc +} + +publishing { + repositories { + maven { + name = 'staging' + url = "${rootProject.buildDir}/local-staging-repo" + } + maven { + name = "Snapshots" // optional target repository name + url = "https://aws.oss.sonatype.org/content/repositories/snapshots" + credentials { + username "$System.env.SONATYPE_USERNAME" + password "$System.env.SONATYPE_PASSWORD" + } + } + } + publications { + shadow(MavenPublication) { publication -> + project.shadow.component(publication) + artifact sourcesJar + artifact javadocJar + + pom { + name = "OpenSearch Security Resource Sharing SPI" + packaging = "jar" + url = "https://github.com/opensearch-project/security" + description = "OpenSearch Security" + scm { + connection = "scm:git@github.com:opensearch-project/security.git" + developerConnection = "scm:git@github.com:opensearch-project/security.git" + url = "git@github.com:opensearch-project/security.git" + } + licenses { + license { + name = "The Apache License, Version 2.0" + url = "http://www.apache.org/licenses/LICENSE-2.0.txt" + } + } + developers { + developer { + name = "OpenSearch" + url = "https://github.com/opensearch-project/security" + } + } + } + } + } + + // TODO - enabled debug logging for the time being, remove this eventually + gradle.startParameter.setShowStacktrace(ShowStacktrace.ALWAYS) + gradle.startParameter.setLogLevel(LogLevel.DEBUG) +} diff --git a/spi/src/main/java/org/opensearch/security/spi/NoopResourceSharingService.java b/spi/src/main/java/org/opensearch/security/spi/NoopResourceSharingService.java new file mode 100644 index 0000000000..0b38533b60 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/NoopResourceSharingService.java @@ -0,0 +1,11 @@ +package org.opensearch.security.spi; + +import org.opensearch.core.action.ActionListener; + +public class NoopResourceSharingService implements ResourceSharingService { + + @Override + public void isSharedWithCurrentUser(String resourceId, ActionListener resourceSharingListener) { + resourceSharingListener.onResponse(Boolean.TRUE); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/ResourceDocVersion.java b/spi/src/main/java/org/opensearch/security/spi/ResourceDocVersion.java new file mode 100644 index 0000000000..f321c2de0b --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/ResourceDocVersion.java @@ -0,0 +1,96 @@ +/* + * 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.spi; + +import java.io.IOException; +import java.util.Locale; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; + +// TODO Job Scheduler keeps track of doc version. Should this keep track of version similarly? + +/** + * Structure to represent resource document version. + */ +public class ResourceDocVersion implements Comparable, Writeable { + private final long primaryTerm; + private final long seqNo; + private final long version; + + public ResourceDocVersion(long primaryTerm, long seqNo, long version) { + this.primaryTerm = primaryTerm; + this.seqNo = seqNo; + this.version = version; + } + + public ResourceDocVersion(StreamInput in) throws IOException { + this.primaryTerm = in.readLong(); + this.seqNo = in.readLong(); + this.version = in.readLong(); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeLong(this.primaryTerm); + out.writeLong(this.seqNo); + out.writeLong(this.version); + } + + public long getPrimaryTerm() { + return primaryTerm; + } + + public long getSeqNo() { + return seqNo; + } + + public long getVersion() { + return version; + } + + /** + * Compare two doc versions. Refer to https://github.com/elastic/elasticsearch/issues/10708 + * + * @param v the doc version to compare. + * @return -1 if this < v, 0 if this == v, otherwise 1; + */ + @Override + public int compareTo(ResourceDocVersion v) { + if (v == null) { + return 1; + } + if (this.seqNo < v.seqNo) { + return -1; + } + if (this.seqNo > v.seqNo) { + return 1; + } + if (this.primaryTerm < v.primaryTerm) { + return -1; + } + if (this.primaryTerm > v.primaryTerm) { + return 1; + } + return 0; + } + + @Override + public String toString() { + return String.format( + Locale.getDefault(), + "{_version: %s, _primary_term: %s, _seq_no: %s}", + this.version, + this.primaryTerm, + this.seqNo + ); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/ResourceParser.java b/spi/src/main/java/org/opensearch/security/spi/ResourceParser.java new file mode 100644 index 0000000000..45b73a2260 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/ResourceParser.java @@ -0,0 +1,9 @@ +package org.opensearch.security.spi; + +import java.io.IOException; + +import org.opensearch.core.xcontent.XContentParser; + +public interface ResourceParser { + T parse(XContentParser xContentParser, String id) throws IOException; +} diff --git a/spi/src/main/java/org/opensearch/security/spi/ResourceSharingExtension.java b/spi/src/main/java/org/opensearch/security/spi/ResourceSharingExtension.java new file mode 100644 index 0000000000..7797b80ba6 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/ResourceSharingExtension.java @@ -0,0 +1,33 @@ +/* + * 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.spi; + +/** + * SPI of security. + */ +public interface ResourceSharingExtension { + /** + * @return resource type string. + */ + String getResourceType(); + + /** + * @return resource index name. + */ + String getResourceIndex(); + + /** + * @return returns a parser for this resource + */ + default ResourceParser getResourceParser() { + return null; + }; + + void assignResourceSharingService(ResourceSharingService service); +} diff --git a/spi/src/main/java/org/opensearch/security/spi/ResourceSharingService.java b/spi/src/main/java/org/opensearch/security/spi/ResourceSharingService.java new file mode 100644 index 0000000000..999cad0593 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/ResourceSharingService.java @@ -0,0 +1,7 @@ +package org.opensearch.security.spi; + +import org.opensearch.core.action.ActionListener; + +public interface ResourceSharingService { + void isSharedWithCurrentUser(String resourceId, ActionListener shareListener); +} diff --git a/spi/src/main/java/org/opensearch/security/spi/SharableResource.java b/spi/src/main/java/org/opensearch/security/spi/SharableResource.java new file mode 100644 index 0000000000..392ca0f3de --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/SharableResource.java @@ -0,0 +1,18 @@ +package org.opensearch.security.spi; + +import java.time.Instant; + +import org.opensearch.core.common.io.stream.NamedWriteable; +import org.opensearch.core.xcontent.ToXContentObject; + +public interface SharableResource extends NamedWriteable, ToXContentObject { + /** + * @return resource name. + */ + String getName(); + + /** + * @return resource last update time. + */ + Instant getLastUpdateTime(); +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/ResourceRequest.java b/spi/src/main/java/org/opensearch/security/spi/actions/ResourceRequest.java new file mode 100644 index 0000000000..798bcce874 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/ResourceRequest.java @@ -0,0 +1,37 @@ +package org.opensearch.security.spi.actions; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +public class ResourceRequest extends ActionRequest { + protected final String resourceIndex; + + /** + * Default constructor + */ + public ResourceRequest(String resourceIndex) { + this.resourceIndex = resourceIndex; + } + + public ResourceRequest(StreamInput in) throws IOException { + this.resourceIndex = in.readString(); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceIndex); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceIndex() { + return this.resourceIndex; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceRequest.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceRequest.java new file mode 100644 index 0000000000..e89f297391 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceRequest.java @@ -0,0 +1,51 @@ +/* + * 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.actions.resource.create; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.security.spi.SharableResource; + +/** + * Request object for CreateSampleResource transport action + */ +public class CreateResourceRequest extends ActionRequest { + + private final T resource; + + /** + * Default constructor + */ + public CreateResourceRequest(T resource) { + this.resource = resource; + } + + public CreateResourceRequest(StreamInput in, Writeable.Reader resourceReader) throws IOException { + this.resource = resourceReader.read(in); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + resource.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public SharableResource getResource() { + return this.resource; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceResponse.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceResponse.java new file mode 100644 index 0000000000..5cbf28c7c9 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceResponse.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.spi.actions.resource.create; + +import java.io.IOException; + +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; + +/** + * Response to a CreateSampleResourceRequest + */ +public class CreateResourceResponse extends ActionResponse implements ToXContentObject { + private final String resourceId; + + /** + * Default constructor + * + * @param resourceId The resourceId + */ + public CreateResourceResponse(String resourceId) { + this.resourceId = resourceId; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(resourceId); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public CreateResourceResponse(final StreamInput in) throws IOException { + resourceId = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resourceId", resourceId); + builder.endObject(); + return builder; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceTransportAction.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceTransportAction.java new file mode 100644 index 0000000000..3802b2495e --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/create/CreateResourceTransportAction.java @@ -0,0 +1,101 @@ +/* + * 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.actions.resource.create; + +import java.io.IOException; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.common.io.stream.Writeable; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.security.spi.SharableResource; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; + +/** + * Transport action for CreateSampleResource. + */ +public class CreateResourceTransportAction extends HandledTransportAction< + CreateResourceRequest, + CreateResourceResponse> { + private static final Logger log = LogManager.getLogger(CreateResourceTransportAction.class); + + private final TransportService transportService; + private final Client nodeClient; + private final String resourceIndex; + + public CreateResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + Client nodeClient, + String actionName, + String resourceIndex, + Writeable.Reader resourceReader + ) { + super(actionName, transportService, actionFilters, (in) -> new CreateResourceRequest(in, resourceReader)); + this.transportService = transportService; + this.nodeClient = nodeClient; + this.resourceIndex = resourceIndex; + } + + @Override + protected void doExecute(Task task, CreateResourceRequest request, ActionListener listener) { + try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + CreateIndexRequest cir = new CreateIndexRequest(resourceIndex); + ActionListener cirListener = ActionListener.wrap( + response -> { createResource(request, listener); }, + (failResponse) -> { + /* Index already exists, ignore and continue */ + createResource(request, listener); + } + ); + nodeClient.admin().indices().create(cir, cirListener); + } + } + + private void createResource(CreateResourceRequest request, ActionListener listener) { + log.warn("Sample name: " + request.getResource()); + SharableResource sample = request.getResource(); + try { + IndexRequest ir = nodeClient.prepareIndex(resourceIndex) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(sample.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .request(); + + log.warn("Index Request: " + ir.toString()); + + // ActionListener resourceSharingListener = ActionListener.wrap(resourceSharingResponse -> { + // listener.onResponse(new CreateResourceResponse("Created resource: " + resourceSharingResponse.toString())); + // }, listener::onFailure); + + ActionListener irListener = ActionListener.wrap(idxResponse -> { + log.info("Created resource: " + idxResponse.toString()); + // ResourceSharingUtils.getInstance() + // .indexResourceSharing(idxResponse.getId(), sample, ShareWith.PUBLIC, resourceSharingListener); + listener.onResponse(new CreateResourceResponse(idxResponse.getId())); + }, listener::onFailure); + nodeClient.index(ir, irListener); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceRequest.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceRequest.java new file mode 100644 index 0000000000..a5b97b89b3 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceRequest.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.actions.resource.get; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.security.spi.actions.ResourceRequest; + +/** + * Request object for GetResource transport action + */ +public class GetResourceRequest extends ResourceRequest { + private final String resourceId; + + /** + * Default constructor + */ + public GetResourceRequest(String resourceId, String resourceIndex) { + super(resourceIndex); + this.resourceId = resourceId; + } + + public GetResourceRequest(StreamInput in) throws IOException { + super(in); + this.resourceId = in.readString(); + } + + public String getResourceId() { + return this.resourceId; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceResponse.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceResponse.java new file mode 100644 index 0000000000..f097bb6f28 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceResponse.java @@ -0,0 +1,57 @@ +/* + * 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.actions.resource.get; + +import java.io.IOException; + +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.common.io.stream.Writeable; +import org.opensearch.core.xcontent.ToXContentObject; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.spi.SharableResource; + +/** + * Response to a GetResourceRequest + */ +public class GetResourceResponse extends ActionResponse implements ToXContentObject { + private final T resource; + + /** + * Default constructor + * + * @param resource The resource + */ + public GetResourceResponse(T resource) { + this.resource = resource; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + resource.writeTo(out); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public GetResourceResponse(final StreamInput in, Writeable.Reader resourceReader) throws IOException { + resource = resourceReader.read(in); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resource", resource); + builder.endObject(); + return builder; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceTransportAction.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceTransportAction.java new file mode 100644 index 0000000000..8dd4a63653 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/get/GetResourceTransportAction.java @@ -0,0 +1,133 @@ +/* + * 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.actions.resource.get; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.OpenSearchException; +import org.opensearch.action.get.GetRequest; +import org.opensearch.action.get.GetResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.security.spi.ResourceParser; +import org.opensearch.security.spi.ResourceSharingService; +import org.opensearch.security.spi.SharableResource; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +/** + * Transport action for GetResource. + */ +public class GetResourceTransportAction extends HandledTransportAction< + GetResourceRequest, + GetResourceResponse> { + private static final Logger log = LogManager.getLogger(GetResourceTransportAction.class); + + private final ResourceSharingService resourceSharingService; + + private final ResourceParser resourceParser; + + private final String resourceIndex; + + private final Client client; + + private final NamedXContentRegistry xContentRegistry; + + public GetResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + String actionName, + String resourceIndex, + ResourceSharingService resourceSharingService, + ResourceParser resourceParser, + Client client, + NamedXContentRegistry xContentRegistry + ) { + super(actionName, transportService, actionFilters, GetResourceRequest::new); + this.resourceSharingService = resourceSharingService; + Objects.requireNonNull(resourceParser); + this.resourceParser = resourceParser; + this.resourceIndex = resourceIndex; + this.client = client; + this.xContentRegistry = xContentRegistry; + } + + @Override + protected void doExecute(Task task, GetResourceRequest request, ActionListener> actionListener) { + getResource(request, actionListener); + } + + private void getResource(GetResourceRequest request, ActionListener> listener) { + ActionListener getResourceListener = ActionListener.wrap(resource -> { + System.out.println("resource: " + resource); + listener.onResponse(new GetResourceResponse(resource)); + }, listener::onFailure); + + try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashContext()) { + GetRequest gr = new GetRequest(resourceIndex); + gr.id(request.getResourceId()); + ActionListener getListener = new ActionListener<>() { + @Override + public void onResponse(GetResponse getResponse) { + try { + XContentParser parser = XContentHelper.createParser( + xContentRegistry, + LoggingDeprecationHandler.INSTANCE, + getResponse.getSourceAsBytesRef(), + XContentType.JSON + ); + T resource = resourceParser.parse(parser, getResponse.getId()); + ActionListener shareListener = new ActionListener<>() { + @Override + public void onResponse(Boolean isShared) { + if (isShared) { + getResourceListener.onResponse(resource); + } else { + getResourceListener.onFailure( + new OpenSearchException("User is not authorized to access this resource") + ); + } + } + + @Override + public void onFailure(Exception e) { + getResourceListener.onFailure( + new OpenSearchException("Failed to check sharing status: " + e.getMessage(), e) + ); + } + }; + + resourceSharingService.isSharedWithCurrentUser(request.getResourceId(), shareListener); + } catch (IOException e) { + throw new OpenSearchException("Caught exception while loading resources: " + e.getMessage()); + } + } + + @Override + public void onFailure(Exception e) { + throw new OpenSearchException("Caught exception while loading resources: " + e.getMessage()); + } + }; + client.get(gr, getListener); + } + // resourceSharingService.getResource(request.getResourceId(), getResourceListener); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceRequest.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceRequest.java new file mode 100644 index 0000000000..09013d226b --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceRequest.java @@ -0,0 +1,33 @@ +/* + * 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.actions.resource.list; + +import java.io.IOException; + +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.security.spi.actions.ResourceRequest; + +/** + * Request object for ListResource transport action + */ +public class ListResourceRequest extends ResourceRequest { + + // TODO Change this into Search instead of List + + /** + * Default constructor + */ + public ListResourceRequest(String resourceIndex) { + super(resourceIndex); + } + + public ListResourceRequest(StreamInput in) throws IOException { + super(in); + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceResponse.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceResponse.java new file mode 100644 index 0000000000..9c321d5732 --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceResponse.java @@ -0,0 +1,57 @@ +/* + * 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.actions.resource.list; + +import java.io.IOException; +import java.util.List; + +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.SharableResource; + +/** + * Response to a ListResourceRequest + */ +public class ListResourceResponse extends ActionResponse implements ToXContentObject { + private final List resources; + + /** + * Default constructor + * + * @param resources The resources + */ + public ListResourceResponse(List resources) { + this.resources = resources; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeList(resources); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public ListResourceResponse(final StreamInput in, Reader resourceReader) throws IOException { + resources = in.readList(resourceReader); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("resources", resources); + builder.endObject(); + return builder; + } +} diff --git a/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceTransportAction.java b/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceTransportAction.java new file mode 100644 index 0000000000..0b11bfa7cb --- /dev/null +++ b/spi/src/main/java/org/opensearch/security/spi/actions/resource/list/ListResourceTransportAction.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.spi.actions.resource.list; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicInteger; + +import org.opensearch.OpenSearchException; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.client.Client; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.LoggingDeprecationHandler; +import org.opensearch.common.xcontent.XContentHelper; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.NamedXContentRegistry; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.index.query.MatchAllQueryBuilder; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.spi.NoopResourceSharingService; +import org.opensearch.security.spi.ResourceParser; +import org.opensearch.security.spi.ResourceSharingService; +import org.opensearch.security.spi.SharableResource; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +/** + * Transport action for ListResource. + */ +public class ListResourceTransportAction extends HandledTransportAction< + ListResourceRequest, + ListResourceResponse> { + private final ResourceSharingService resourceSharingService; + + private final ResourceParser resourceParser; + + private final Client client; + + private final String resourceIndex; + + private final NamedXContentRegistry xContentRegistry; + + public ListResourceTransportAction( + TransportService transportService, + ActionFilters actionFilters, + String actionName, + String resourceIndex, + ResourceSharingService resourceSharingService, + ResourceParser resourceParser, + Client client, + NamedXContentRegistry xContentRegistry + ) { + super(actionName, transportService, actionFilters, ListResourceRequest::new); + this.client = client; + this.resourceSharingService = resourceSharingService != null ? resourceSharingService : new NoopResourceSharingService(); + this.resourceIndex = resourceIndex; + this.xContentRegistry = xContentRegistry; + Objects.requireNonNull(resourceParser); + this.resourceParser = resourceParser; + } + + @Override + protected void doExecute(Task task, ListResourceRequest request, ActionListener> listener) { + ActionListener> listResourceListener = ActionListener.wrap(resourcesList -> { + System.out.println("resourcesList: " + resourcesList); + listener.onResponse(new ListResourceResponse<>(resourcesList)); + }, listener::onFailure); + try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashContext()) { + SearchRequest sr = new SearchRequest(resourceIndex); + SearchSourceBuilder matchAllQuery = new SearchSourceBuilder(); + matchAllQuery.query(new MatchAllQueryBuilder()); + sr.source(matchAllQuery); + ActionListener searchListener = new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + List resources = new ArrayList<>(); + + SearchHit[] hits = searchResponse.getHits().getHits(); + + if (hits.length == 0) { + listResourceListener.onResponse(resources); + return; + } + + AtomicInteger remainingChecks = new AtomicInteger(hits.length); + + for (SearchHit hit : hits) { + try { + XContentParser parser = XContentHelper.createParser( + xContentRegistry, + LoggingDeprecationHandler.INSTANCE, + hit.getSourceRef(), + XContentType.JSON + ); + T resource = resourceParser.parse(parser, hit.getId()); + + ActionListener shareListener = new ActionListener<>() { + @Override + public void onResponse(Boolean isShared) { + if (isShared) { + synchronized (resources) { + resources.add(resource); + } + } + if (remainingChecks.decrementAndGet() == 0) { + listResourceListener.onResponse(resources); + } + } + + @Override + public void onFailure(Exception e) { + listResourceListener.onFailure( + new OpenSearchException("Failed to check sharing status: " + e.getMessage(), e) + ); + } + }; + + resourceSharingService.isSharedWithCurrentUser(hit.getId(), shareListener); + + } catch (IOException e) { + listResourceListener.onFailure( + new OpenSearchException("Caught exception while loading resources: " + e.getMessage(), e) + ); + return; + } + } + listResourceListener.onResponse(resources); + } + + @Override + public void onFailure(Exception e) { + throw new OpenSearchException("Caught exception while loading resources: " + e.getMessage()); + } + }; + client.search(sr, searchListener); + } + } +} diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 25ab6c0c06..095dc88e31 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -43,6 +43,7 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -116,6 +117,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; @@ -173,16 +175,22 @@ import org.opensearch.security.privileges.RestLayerPrivilegesEvaluator; import org.opensearch.security.privileges.dlsfls.DlsFlsBaseContext; import org.opensearch.security.resolver.IndexResolverReplacer; +import org.opensearch.security.resource.ResourceSharingListener; +import org.opensearch.security.resource.SecurityResourceSharingService; import org.opensearch.security.rest.DashboardsInfoAction; import org.opensearch.security.rest.SecurityConfigUpdateAction; import org.opensearch.security.rest.SecurityHealthAction; import org.opensearch.security.rest.SecurityInfoAction; import org.opensearch.security.rest.SecurityWhoAmIAction; import org.opensearch.security.rest.TenantInfoAction; +import org.opensearch.security.rest.resource.ShareWithAction; +import org.opensearch.security.rest.resource.ShareWithTransportAction; import org.opensearch.security.securityconf.DynamicConfigFactory; import org.opensearch.security.securityconf.impl.CType; import org.opensearch.security.setting.OpensearchDynamicSetting; import org.opensearch.security.setting.TransportPassiveAuthSetting; +import org.opensearch.security.spi.ResourceSharingExtension; +import org.opensearch.security.spi.ResourceSharingService; import org.opensearch.security.ssl.ExternalSecurityKeyStore; import org.opensearch.security.ssl.OpenSearchSecureSettingsFactory; import org.opensearch.security.ssl.OpenSearchSecuritySSLPlugin; @@ -220,6 +228,7 @@ import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.ENDPOINTS_WITH_PERMISSIONS; import static org.opensearch.security.dlic.rest.api.RestApiAdminPrivilegesEvaluator.SECURITY_CONFIG_UPDATE; +import static org.opensearch.security.resource.ResourceSharingListener.RESOURCE_SHARING_INDEX; import static org.opensearch.security.setting.DeprecatedSettings.checkForDeprecatedSetting; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX; import static org.opensearch.security.support.ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_USE_CLUSTER_STATE; @@ -234,6 +243,7 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin ClusterPlugin, MapperPlugin, // CS-SUPPRESS-SINGLE: RegexpSingleline get Extensions Settings + ExtensiblePlugin, ExtensionAwarePlugin, IdentityPlugin // CS-ENFORCE-SINGLE @@ -271,6 +281,10 @@ public final class OpenSearchSecurityPlugin extends OpenSearchSecuritySSLPlugin private volatile OpensearchDynamicSetting transportPassiveAuthSetting; private volatile PasswordHasher passwordHasher; private volatile DlsFlsBaseContext dlsFlsBaseContext; + private final Set indicesToListen = new HashSet<>(); + // CS-SUPPRESS-SINGLE: RegexpSingleline SPI Extensions are unrelated to OpenSearch extensions + private final List resourceSharingExtensions = new ArrayList<>(); + // CS-ENFORCE-SINGLE public static boolean isActionTraceEnabled() { @@ -658,7 +672,8 @@ public List getRestHandlers( sslSettingsManager, Objects.requireNonNull(userService), sslCertReloadEnabled, - passwordHasher + passwordHasher, + resourceSharingExtensions ) ); log.debug("Added {} rest handler(s)", handlers.size()); @@ -688,6 +703,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre actions.add(new ActionHandler<>(CertificatesActionType.INSTANCE, TransportCertificatesInfoNodesAction.class)); } actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); + actions.add(new ActionHandler<>(ShareWithAction.INSTANCE, ShareWithTransportAction.class)); } return actions; } @@ -715,6 +731,11 @@ public void onIndexModule(IndexModule indexModule) { dlsFlsBaseContext ) ); + System.out.println("this.indicesToListen: " + this.indicesToListen); + if (this.indicesToListen.contains(indexModule.getIndex().getName())) { + indexModule.addIndexOperationListener(ResourceSharingListener.getInstance()); + log.warn("Security started listening to operations on index {}", indexModule.getIndex().getName()); + } indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @Override @@ -1028,7 +1049,6 @@ public Collection createComponents( IndexNameExpressionResolver indexNameExpressionResolver, Supplier repositoriesServiceSupplier ) { - SSLConfig.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); if (SSLConfig.isSslOnlyMode()) { return super.createComponents( @@ -1056,6 +1076,14 @@ public Collection createComponents( return components; } + ResourceSharingListener.getInstance().initialize(threadPool, localClient); + // CS-SUPPRESS-SINGLE: RegexpSingleline SPI Extensions are unrelated to OpenSearch extensions + for (ResourceSharingExtension extension : resourceSharingExtensions) { + ResourceSharingService resourceSharingService = new SecurityResourceSharingService(localClient, extension.getResourceIndex()); + extension.assignResourceSharingService(resourceSharingService); + } + // CS-ENFORCE-SINGLE + // Register opensearch dynamic settings transportPassiveAuthSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); @@ -2130,7 +2158,11 @@ public Collection getSystemIndexDescriptors(Settings sett ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX ); final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); - return Collections.singletonList(systemIndexDescriptor); + final SystemIndexDescriptor resourceSharingIndexDescriptor = new SystemIndexDescriptor( + RESOURCE_SHARING_INDEX, + "Resource sharing index" + ); + return List.of(systemIndexDescriptor, resourceSharingIndexDescriptor); } @Override @@ -2162,6 +2194,20 @@ public Optional getSecureSettingFactory(Settings settings ); } + // CS-SUPPRESS-SINGLE: RegexpSingleline SPI Extensions are unrelated to OpenSearch extensions + @Override + public void loadExtensions(ExtensiblePlugin.ExtensionLoader loader) { + System.out.println("loadExtensions"); + for (ResourceSharingExtension extension : loader.loadExtensions(ResourceSharingExtension.class)) { + String resourceIndexName = extension.getResourceIndex(); + System.out.println("localClient: " + localClient); + this.indicesToListen.add(resourceIndexName); + resourceSharingExtensions.add(extension); + log.info("Loaded resource, index: {}", resourceIndexName); + } + } + // CS-ENFORCE-SINGLE + @SuppressWarnings("removal") private void tryAddSecurityProvider() { final SecurityManager sm = System.getSecurityManager(); diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 0b00bcf943..b50b6082fb 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -223,7 +223,9 @@ public boolean authenticate(final SecurityRequestChannel request) { if (adminDns.isAdminDN(sslPrincipal)) { // PKI authenticated REST call - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, new User(sslPrincipal)); + User superuser = new User(sslPrincipal); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, superuser); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, superuser); auditLog.logSucceededLogin(sslPrincipal, true, null, request); return true; } @@ -389,6 +391,11 @@ public boolean authenticate(final SecurityRequestChannel request) { final User impersonatedUser = impersonate(request, authenticatedUser); threadPool.getThreadContext() .putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, impersonatedUser == null ? authenticatedUser : impersonatedUser); + threadPool.getThreadContext() + .putPersistent( + ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, + impersonatedUser == null ? authenticatedUser : impersonatedUser + ); auditLog.logSucceededLogin( (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), false, @@ -422,6 +429,7 @@ public boolean authenticate(final SecurityRequestChannel request) { anonymousUser.setRequestedTenant(tenant); threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, anonymousUser); + threadPool.getThreadContext().putPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER, anonymousUser); auditLog.logSucceededLogin(anonymousUser.getName(), false, null, request); if (isDebugEnabled) { log.debug("Anonymous User is authenticated"); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java index 498230423f..7d3be960b3 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityApiDependencies.java @@ -11,11 +11,14 @@ package org.opensearch.security.dlic.rest.api; +import java.util.List; + import org.opensearch.common.settings.Settings; import org.opensearch.security.auditlog.AuditLog; import org.opensearch.security.configuration.AdminDNs; import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.spi.ResourceSharingExtension; import org.opensearch.security.support.ConfigConstants; public class SecurityApiDependencies { @@ -25,6 +28,7 @@ public class SecurityApiDependencies { private final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator; private final AuditLog auditLog; private final Settings settings; + private final List resourceSharingExtensions; private final PrivilegesEvaluator privilegesEvaluator; @@ -35,7 +39,8 @@ public SecurityApiDependencies( final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator, final RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator, final AuditLog auditLog, - final Settings settings + final Settings settings, + final List resourceSharingExtensions ) { this.adminDNs = adminDNs; this.configurationRepository = configurationRepository; @@ -44,6 +49,7 @@ public SecurityApiDependencies( this.restApiAdminPrivilegesEvaluator = restApiAdminPrivilegesEvaluator; this.auditLog = auditLog; this.settings = settings; + this.resourceSharingExtensions = resourceSharingExtensions; } public AdminDNs adminDNs() { @@ -66,6 +72,10 @@ public RestApiAdminPrivilegesEvaluator restApiAdminPrivilegesEvaluator() { return restApiAdminPrivilegesEvaluator; } + public List resourceSharingExtensions() { + return resourceSharingExtensions; + } + public AuditLog auditLog() { return auditLog; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index c28a1bdc1d..d8c5402f81 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -25,6 +25,8 @@ import org.opensearch.security.configuration.ConfigurationRepository; import org.opensearch.security.hasher.PasswordHasher; import org.opensearch.security.privileges.PrivilegesEvaluator; +import org.opensearch.security.rest.resource.ShareWithRestAction; +import org.opensearch.security.spi.ResourceSharingExtension; import org.opensearch.security.ssl.SslSettingsManager; import org.opensearch.security.ssl.transport.PrincipalExtractor; import org.opensearch.security.user.UserService; @@ -49,7 +51,8 @@ public static Collection getHandler( final SslSettingsManager sslSettingsManager, final UserService userService, final boolean certificatesReloadEnabled, - final PasswordHasher passwordHasher + final PasswordHasher passwordHasher, + final List resourceSharingExtensions ) { final var securityApiDependencies = new SecurityApiDependencies( adminDns, @@ -63,7 +66,8 @@ public static Collection getHandler( settings.getAsBoolean(SECURITY_RESTAPI_ADMIN_ENABLED, false) ), auditLog, - settings + settings, + resourceSharingExtensions ); return List.of( new InternalUsersApiAction(clusterService, threadPool, userService, securityApiDependencies, passwordHasher), @@ -104,7 +108,8 @@ public static Collection getHandler( certificatesReloadEnabled, securityApiDependencies ), - new CertificatesApiAction(clusterService, threadPool, securityApiDependencies) + new CertificatesApiAction(clusterService, threadPool, securityApiDependencies), + new ShareWithRestAction(resourceSharingExtensions) ); } diff --git a/src/main/java/org/opensearch/security/resource/ResourceSharingEntry.java b/src/main/java/org/opensearch/security/resource/ResourceSharingEntry.java new file mode 100644 index 0000000000..d7ecfac33c --- /dev/null +++ b/src/main/java/org/opensearch/security/resource/ResourceSharingEntry.java @@ -0,0 +1,67 @@ +/* + * 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.resource; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.security.rest.resource.ShareWith; + +public class ResourceSharingEntry implements ToXContentFragment { + private final String resourceIndex; + private final String resourceId; + private final ResourceUser resourceUser; + // Key to this map is an action group + private final Map shareWith; + + public ResourceSharingEntry(String resourceIndex, String resourceId, ResourceUser resourceUser, Map shareWith) { + this.resourceIndex = resourceIndex; + this.resourceId = resourceId; + this.resourceUser = resourceUser; + this.shareWith = shareWith; + } + + @SuppressWarnings("unchecked") + public static ResourceSharingEntry fromSource(Map sourceAsMap) { + String resourceIndex = (String) sourceAsMap.get("resource_index"); + String resourceId = (String) sourceAsMap.get("resource_id"); + ResourceUser resourceUser = ResourceUser.fromSource((Map) sourceAsMap.get("resource_user")); + Map sharedWithMap = (Map) sourceAsMap.get("share_with"); + Map sharedWith = new HashMap<>(); + for (Map.Entry entry : sharedWithMap.entrySet()) { + ShareWith shareWith = ShareWith.fromSource((Map) entry.getValue()); + sharedWith.put(entry.getKey(), shareWith); + } + return new ResourceSharingEntry(resourceIndex, resourceId, resourceUser, sharedWith); + } + + public ResourceUser getResourceUser() { + return resourceUser; + } + + public Map getShareWith() { + return shareWith; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field("resource_index", resourceIndex) + .field("resource_id", resourceId) + .field("resource_user", resourceUser) + .field("share_with", shareWith) + .endObject(); + } +} diff --git a/src/main/java/org/opensearch/security/resource/ResourceSharingListener.java b/src/main/java/org/opensearch/security/resource/ResourceSharingListener.java new file mode 100644 index 0000000000..236efea7cb --- /dev/null +++ b/src/main/java/org/opensearch/security/resource/ResourceSharingListener.java @@ -0,0 +1,139 @@ +/* + * 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.resource; + +import java.io.IOException; +import java.util.Map; +import java.util.concurrent.Callable; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.admin.indices.create.CreateIndexRequest; +import org.opensearch.action.admin.indices.create.CreateIndexResponse; +import org.opensearch.action.index.IndexRequest; +import org.opensearch.action.index.IndexResponse; +import org.opensearch.action.support.WriteRequest; +import org.opensearch.client.Client; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.index.shard.ShardId; +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.index.engine.Engine; +import org.opensearch.index.shard.IndexingOperationListener; +import org.opensearch.security.rest.resource.ShareWith; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.user.User; +import org.opensearch.threadpool.ThreadPool; + +import static org.opensearch.common.xcontent.XContentFactory.jsonBuilder; + +public class ResourceSharingListener implements IndexingOperationListener { + private final static Logger log = LogManager.getLogger(ResourceSharingListener.class); + + private static final ResourceSharingListener INSTANCE = new ResourceSharingListener(); + + public static final String RESOURCE_SHARING_INDEX = ".resource-sharing"; + + private static final String UNLIMITED = "unlimited"; + + private boolean initialized; + private ThreadPool threadPool; + private Client client; + + private ResourceSharingListener() {} + + public static ResourceSharingListener getInstance() { + return ResourceSharingListener.INSTANCE; + } + + public void initialize(ThreadPool threadPool, Client client) { + if (initialized) { + return; + } + initialized = true; + this.threadPool = threadPool; + this.client = client; + } + + public boolean isInitialized() { + return initialized; + } + + @Override + public void postIndex(ShardId shardId, Engine.Index index, Engine.IndexResult result) { + log.warn("postIndex called on " + shardId.getIndexName()); + String resourceId = index.id(); + String resourceIndex = shardId.getIndexName(); + System.out.println("postIndex called on " + shardId.getIndexName()); + System.out.println("resourceId: " + resourceId); + System.out.println("resourceIndex: " + resourceIndex); + User authenticatedUser = (User) client.threadPool() + .getThreadContext() + .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); + System.out.println("resourceUser: " + authenticatedUser); + ResourceUser resourceUser = new ResourceUser(authenticatedUser.getName(), authenticatedUser.getRoles()); + try { + indexResourceSharing(resourceId, resourceIndex, resourceUser, ShareWith.PRIVATE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void postDelete(ShardId shardId, Engine.Delete delete, Engine.DeleteResult result) { + log.warn("postDelete called on " + shardId.getIndexName()); + } + + private void createResourceSharingIndexIfNotExists(Callable callable) { + try (ThreadContext.StoredContext ctx = this.threadPool.getThreadContext().stashContext()) { + CreateIndexRequest cir = new CreateIndexRequest(RESOURCE_SHARING_INDEX); + ActionListener cirListener = ActionListener.wrap(response -> { + log.warn(RESOURCE_SHARING_INDEX + " created."); + callable.call(); + }, (failResponse) -> { + /* Index already exists, ignore and continue */ + log.warn(RESOURCE_SHARING_INDEX + " exists."); + try { + callable.call(); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + this.client.admin().indices().create(cir, cirListener); + } + } + + public void indexResourceSharing(String resourceId, String resourceIndex, ResourceUser resourceUser, ShareWith shareWith) + throws IOException { + createResourceSharingIndexIfNotExists(() -> { + ResourceSharingEntry entry = new ResourceSharingEntry(resourceIndex, resourceId, resourceUser, Map.of(UNLIMITED, shareWith)); + + IndexRequest ir = client.prepareIndex(RESOURCE_SHARING_INDEX) + .setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE) + .setSource(entry.toXContent(jsonBuilder(), ToXContent.EMPTY_PARAMS)) + .request(); + + log.warn("Index Request: " + ir.toString()); + + ActionListener irListener = ActionListener.wrap( + idxResponse -> { log.warn("Created " + RESOURCE_SHARING_INDEX + " entry."); }, + (failResponse) -> { + log.error(failResponse.getMessage()); + log.error("Failed to create " + RESOURCE_SHARING_INDEX + " entry."); + } + ); + client.index(ir, irListener); + return null; + }); + } +} diff --git a/src/main/java/org/opensearch/security/resource/ResourceUser.java b/src/main/java/org/opensearch/security/resource/ResourceUser.java new file mode 100644 index 0000000000..1c135f6f01 --- /dev/null +++ b/src/main/java/org/opensearch/security/resource/ResourceUser.java @@ -0,0 +1,51 @@ +/* + * 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.resource; + +import java.io.IOException; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.opensearch.core.xcontent.ToXContent; +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; + +public class ResourceUser implements ToXContentFragment { + private final String name; + + private final Set backendRoles; + + public ResourceUser(String name, Set backendRoles) { + this.name = name; + this.backendRoles = backendRoles; + } + + public String getName() { + return name; + } + + public Set getBackendRoles() { + return backendRoles; + } + + @SuppressWarnings("unchecked") + public static ResourceUser fromSource(Map sourceAsMap) { + String name = (String) sourceAsMap.get("name"); + Set backendRoles = new HashSet<>((List) sourceAsMap.get("backend_roles")); + return new ResourceUser(name, backendRoles); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, ToXContent.Params params) throws IOException { + return builder.startObject().field("name", name).field("backend_roles", backendRoles).endObject(); + } +} diff --git a/src/main/java/org/opensearch/security/resource/SecurityResourceSharingService.java b/src/main/java/org/opensearch/security/resource/SecurityResourceSharingService.java new file mode 100644 index 0000000000..13f7308984 --- /dev/null +++ b/src/main/java/org/opensearch/security/resource/SecurityResourceSharingService.java @@ -0,0 +1,105 @@ +/* + * 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.resource; + +import org.opensearch.OpenSearchException; +import org.opensearch.ResourceNotFoundException; +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.client.Client; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.core.action.ActionListener; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.security.rest.resource.ShareWith; +import org.opensearch.security.spi.ResourceSharingService; +import org.opensearch.security.support.ConfigConstants; +import org.opensearch.security.support.WildcardMatcher; +import org.opensearch.security.user.User; + +import static org.opensearch.security.resource.ResourceSharingListener.RESOURCE_SHARING_INDEX; + +public class SecurityResourceSharingService implements ResourceSharingService { + private final Client client; + private final String resourceIndex; + + public SecurityResourceSharingService(Client client, String resourceIndex) { + this.client = client; + this.resourceIndex = resourceIndex; + } + + private boolean hasPermissionsFor(User authenticatedUser, ResourceSharingEntry sharedWith) { + // 1. The resource_user is the currently authenticated user + // 2. The resource has been shared with the authenticated user + // 3. The resource has been shared with a backend role that the authenticated user has + if (authenticatedUser.getName().equals(sharedWith.getResourceUser().getName())) { + return true; + } + + for (ShareWith shareWith : sharedWith.getShareWith().values()) { + WildcardMatcher userMatcher = WildcardMatcher.from(shareWith.getUsers()); + if (userMatcher.test(authenticatedUser.getName())) { + return true; + } + WildcardMatcher backendRoleMatcher = WildcardMatcher.from(shareWith.getBackendRoles()); + if (authenticatedUser.getRoles().stream().anyMatch(backendRoleMatcher::test)) { + return true; + } + } + return false; + } + + @Override + public void isSharedWithCurrentUser(String resourceId, ActionListener resourceSharingListener) { + User authenticatedUser = (User) client.threadPool() + .getThreadContext() + .getPersistent(ConfigConstants.OPENDISTRO_SECURITY_AUTHENTICATED_USER); + try (ThreadContext.StoredContext ignore = client.threadPool().getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(RESOURCE_SHARING_INDEX); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.matchQuery("resource_index", resourceIndex)) + .must(QueryBuilders.matchQuery("resource_id", resourceId)); + + searchSourceBuilder.query(boolQuery); + searchSourceBuilder.size(1); // Limit to 1 result + searchRequest.source(searchSourceBuilder); + + ActionListener searchListener = new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + SearchHit[] hits = searchResponse.getHits().getHits(); + if (hits.length > 0) { + SearchHit hit = hits[0]; + ResourceSharingEntry sharedWith = ResourceSharingEntry.fromSource(hit.getSourceAsMap()); + if (hasPermissionsFor(authenticatedUser, sharedWith)) { + resourceSharingListener.onResponse(Boolean.TRUE); + } else { + resourceSharingListener.onResponse(Boolean.FALSE); + } + } else { + resourceSharingListener.onFailure(new ResourceNotFoundException("Resource not found")); + } + } + + @Override + public void onFailure(Exception e) { + throw new OpenSearchException("Caught exception while loading resources: " + e.getMessage()); + } + }; + + client.search(searchRequest, searchListener); + } + } +} diff --git a/src/main/java/org/opensearch/security/rest/resource/ShareWith.java b/src/main/java/org/opensearch/security/rest/resource/ShareWith.java new file mode 100644 index 0000000000..341fde9153 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resource/ShareWith.java @@ -0,0 +1,69 @@ +/* + * 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.rest.resource; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +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; + +public class ShareWith implements NamedWriteable, ToXContentFragment { + + public final static ShareWith PRIVATE = new ShareWith(List.of(), List.of()); + public final static ShareWith PUBLIC = new ShareWith(List.of("*"), List.of("*")); + + private final List users; + private final List backendRoles; + + public ShareWith(List users, List backendRoles) { + this.users = users; + this.backendRoles = backendRoles; + } + + public ShareWith(StreamInput in) throws IOException { + this.users = in.readStringList(); + this.backendRoles = in.readStringList(); + } + + @SuppressWarnings("unchecked") + public static ShareWith fromSource(Map sourceAsMap) { + List users = (List) sourceAsMap.get("users"); + List backendRoles = (List) sourceAsMap.get("backend_roles"); + return new ShareWith(users, backendRoles); + } + + public List getUsers() { + return users; + } + + public List getBackendRoles() { + return backendRoles; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject().field("users", users).field("backend_roles", backendRoles).endObject(); + } + + @Override + public String getWriteableName() { + return "share_with"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeStringCollection(users); + out.writeStringCollection(backendRoles); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resource/ShareWithAction.java b/src/main/java/org/opensearch/security/rest/resource/ShareWithAction.java new file mode 100644 index 0000000000..7e0f52ce4e --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resource/ShareWithAction.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.rest.resource; + +import org.opensearch.action.ActionType; + +/** + * Action to update sharing configuration for a sample resource + */ +public class ShareWithAction extends ActionType { + /** + * Update sharing configuration for sample resource action instance + */ + public static final ShareWithAction INSTANCE = new ShareWithAction(); + /** + * Update sharing configuration for sample resource action name + */ + public static final String NAME = "cluster:admin/opendistro_security/resource/share_with"; + + private ShareWithAction() { + super(NAME, ShareWithResponse::new); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resource/ShareWithRequest.java b/src/main/java/org/opensearch/security/rest/resource/ShareWithRequest.java new file mode 100644 index 0000000000..e7404e8b9e --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resource/ShareWithRequest.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.rest.resource; + +import java.io.IOException; + +import org.opensearch.action.ActionRequest; +import org.opensearch.action.ActionRequestValidationException; +import org.opensearch.core.common.io.stream.StreamInput; +import org.opensearch.core.common.io.stream.StreamOutput; + +/** + * Request object for UpdateResourceSharing transport action + */ +public class ShareWithRequest extends ActionRequest { + + private final String resourceId; + private final String resourceIndex; + private final ShareWith shareWith; + + /** + * Default constructor + */ + public ShareWithRequest(String resourceId, String resourceIndex, ShareWith shareWith) { + this.resourceId = resourceId; + this.resourceIndex = resourceIndex; + this.shareWith = shareWith; + } + + public ShareWithRequest(StreamInput in) throws IOException { + this.resourceId = in.readString(); + this.resourceIndex = in.readString(); + this.shareWith = new ShareWith(in); + } + + @Override + public void writeTo(final StreamOutput out) throws IOException { + out.writeString(resourceId); + out.writeString(resourceIndex); + shareWith.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + public String getResourceId() { + return this.resourceId; + } + + public String getResourceIndex() { + return this.resourceIndex; + } + + public ShareWith getShareWith() { + return this.shareWith; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resource/ShareWithResponse.java b/src/main/java/org/opensearch/security/rest/resource/ShareWithResponse.java new file mode 100644 index 0000000000..a33529091e --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resource/ShareWithResponse.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.rest.resource; + +import java.io.IOException; + +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; + +/** + * Response to a UpdateResourceSharingRequest + */ +public class ShareWithResponse extends ActionResponse implements ToXContentObject { + private final String message; + + /** + * Default constructor + * + * @param message The message + */ + public ShareWithResponse(String message) { + this.message = message; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(message); + } + + /** + * Constructor with StreamInput + * + * @param in the stream input + */ + public ShareWithResponse(final StreamInput in) throws IOException { + message = in.readString(); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("message", message); + builder.endObject(); + return builder; + } +} diff --git a/src/main/java/org/opensearch/security/rest/resource/ShareWithRestAction.java b/src/main/java/org/opensearch/security/rest/resource/ShareWithRestAction.java new file mode 100644 index 0000000000..d114b8a576 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resource/ShareWithRestAction.java @@ -0,0 +1,78 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.security.rest.resource; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.ImmutableList; + +import org.opensearch.client.node.NodeClient; +import org.opensearch.core.xcontent.XContentParser; +import org.opensearch.rest.BaseRestHandler; +import org.opensearch.rest.RestRequest; +import org.opensearch.rest.action.RestToXContentListener; +import org.opensearch.security.spi.ResourceSharingExtension; + +import static org.opensearch.rest.RestRequest.Method.PUT; +import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; + +public class ShareWithRestAction extends BaseRestHandler { + + private final Map resourceTypeToIndexMap = new HashMap<>(); + + public ShareWithRestAction(final List resourceSharingExtensions) { + if (resourceSharingExtensions != null) { + for (ResourceSharingExtension resourceSharingExtension : resourceSharingExtensions) { + resourceTypeToIndexMap.put(resourceSharingExtension.getResourceType(), resourceSharingExtension.getResourceIndex()); + } + } + } + + private static final List routes = addRoutesPrefix( + ImmutableList.of(new Route(PUT, "/resource/{resource_type}/{id}/share_with")), + "/_plugins/_security" + ); + + @Override + public List routes() { + return routes; + } + + @Override + public String getName() { + return "update_resource_sharing"; + } + + @SuppressWarnings("unchecked") + @Override + public RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + String resourceId = request.param("id"); + String resourceType = request.param("resource_type"); + + if (!resourceTypeToIndexMap.containsKey(resourceType)) { + throw new IllegalArgumentException("Resource type " + resourceType + " is not supported"); + } + + System.out.println("share with endpoint"); + System.out.println("resourceId: " + resourceId); + Map source; + try (XContentParser parser = request.contentParser()) { + source = parser.map(); + } + + Map shareWithMap = (Map) source.get("share_with"); + ShareWith shareWith = new ShareWith((List) shareWithMap.get("users"), (List) shareWithMap.get("backend_roles")); + + final ShareWithRequest shareWithRequest = new ShareWithRequest(resourceId, resourceTypeToIndexMap.get(resourceType), shareWith); + return channel -> client.executeLocally(ShareWithAction.INSTANCE, shareWithRequest, new RestToXContentListener<>(channel)); + } +} diff --git a/src/main/java/org/opensearch/security/rest/resource/ShareWithTransportAction.java b/src/main/java/org/opensearch/security/rest/resource/ShareWithTransportAction.java new file mode 100644 index 0000000000..8da088ab13 --- /dev/null +++ b/src/main/java/org/opensearch/security/rest/resource/ShareWithTransportAction.java @@ -0,0 +1,111 @@ +/* + * 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.rest.resource; + +import java.io.IOException; +import java.util.Objects; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import org.opensearch.action.search.SearchRequest; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.support.ActionFilters; +import org.opensearch.action.support.HandledTransportAction; +import org.opensearch.action.update.UpdateRequest; +import org.opensearch.action.update.UpdateResponse; +import org.opensearch.client.Client; +import org.opensearch.common.inject.Inject; +import org.opensearch.common.util.concurrent.ThreadContext; +import org.opensearch.common.xcontent.XContentFactory; +import org.opensearch.core.action.ActionListener; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.index.query.BoolQueryBuilder; +import org.opensearch.index.query.QueryBuilders; +import org.opensearch.search.SearchHit; +import org.opensearch.search.builder.SearchSourceBuilder; +import org.opensearch.tasks.Task; +import org.opensearch.transport.TransportService; + +/** + * Transport action for UpdateResourceSharing. + */ +public class ShareWithTransportAction extends HandledTransportAction { + private static final Logger log = LogManager.getLogger(ShareWithTransportAction.class); + + public static final String RESOURCE_SHARING_INDEX = ".resource-sharing"; + + private final TransportService transportService; + private final Client nodeClient; + + @Inject + public ShareWithTransportAction(TransportService transportService, ActionFilters actionFilters, Client nodeClient) { + super(ShareWithAction.NAME, transportService, actionFilters, ShareWithRequest::new); + this.transportService = transportService; + this.nodeClient = nodeClient; + } + + @Override + protected void doExecute(Task task, ShareWithRequest request, ActionListener listener) { + try (ThreadContext.StoredContext ignore = transportService.getThreadPool().getThreadContext().stashContext()) { + SearchRequest searchRequest = new SearchRequest(RESOURCE_SHARING_INDEX); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.matchQuery("resource_id", request.getResourceId())) + .must(QueryBuilders.matchQuery("resource_index", request.getResourceIndex())); + searchSourceBuilder.query(boolQuery); + searchRequest.source(searchSourceBuilder); + + nodeClient.search(searchRequest, new ActionListener<>() { + @Override + public void onResponse(SearchResponse searchResponse) { + if (Objects.requireNonNull(searchResponse.getHits().getTotalHits()).value == 1) { + SearchHit hit = searchResponse.getHits().getAt(0); + UpdateRequest updateRequest = new UpdateRequest(RESOURCE_SHARING_INDEX, hit.getId()); + try { + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject(); + { + builder.startObject("share_with"); + { + builder.field("users", request.getShareWith().getUsers()); + builder.field("backend_roles", request.getShareWith().getBackendRoles()); + } + builder.endObject(); + } + builder.endObject(); + updateRequest.doc(builder); + + nodeClient.update(updateRequest, new ActionListener<>() { + @Override + public void onResponse(UpdateResponse updateResponse) { + listener.onResponse(new ShareWithResponse("success")); + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } catch (IOException e) { + listener.onFailure(e); + } + } else { + listener.onFailure(new IllegalStateException(".resource-sharing entry not found")); + } + } + + @Override + public void onFailure(Exception e) { + listener.onFailure(e); + } + }); + } + } +} diff --git a/src/main/java/org/opensearch/security/support/ConfigConstants.java b/src/main/java/org/opensearch/security/support/ConfigConstants.java index 444c77f0f4..6ef280b2b8 100644 --- a/src/main/java/org/opensearch/security/support/ConfigConstants.java +++ b/src/main/java/org/opensearch/security/support/ConfigConstants.java @@ -113,6 +113,7 @@ public class ConfigConstants { 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_AUTHENTICATED_USER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "authenticated_user"; public static final String OPENDISTRO_SECURITY_USER_HEADER = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_header"; public static final String OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT = OPENDISTRO_SECURITY_CONFIG_PREFIX + "user_info"; diff --git a/src/main/java/org/opensearch/security/user/UserFragment.java b/src/main/java/org/opensearch/security/user/UserFragment.java new file mode 100644 index 0000000000..b0c9164404 --- /dev/null +++ b/src/main/java/org/opensearch/security/user/UserFragment.java @@ -0,0 +1,34 @@ +/* + * 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.user; + +import java.io.IOException; + +import org.opensearch.core.xcontent.ToXContentFragment; +import org.opensearch.core.xcontent.XContentBuilder; + +public class UserFragment implements ToXContentFragment { + private final User user; + + public UserFragment(User user) { + this.user = user; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.startObject() + .field("username", user.getName()) + .field("roles", user.getSecurityRoles()) + .field("backend_roles", user.getRoles()) + .endObject(); + } +} diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java index 9278551efa..f5237115d1 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/AbstractApiActionValidationTest.java @@ -78,7 +78,8 @@ public void setup() { null, restApiAdminPrivilegesEvaluator, null, - Settings.EMPTY + Settings.EMPTY, + List.of() ); passwordHasher = PasswordHasherFactory.createPasswordHasher( diff --git a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java index a6832457b3..b164d9a245 100644 --- a/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java +++ b/src/test/java/org/opensearch/security/dlic/rest/api/SecurityConfigApiActionValidationTest.java @@ -11,6 +11,8 @@ package org.opensearch.security.dlic.rest.api; +import java.util.List; + import org.junit.Test; import org.opensearch.common.settings.Settings; @@ -30,7 +32,16 @@ public void accessHandlerForDefaultSettings() { final var securityConfigApiAction = new SecurityConfigApiAction( clusterService, threadPool, - new SecurityApiDependencies(null, configurationRepository, null, null, restApiAdminPrivilegesEvaluator, null, Settings.EMPTY) + new SecurityApiDependencies( + null, + configurationRepository, + null, + null, + restApiAdminPrivilegesEvaluator, + null, + Settings.EMPTY, + List.of() + ) ); assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build())); assertFalse(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.PUT).build())); @@ -49,7 +60,8 @@ public void accessHandlerForUnsupportedSetting() { null, restApiAdminPrivilegesEvaluator, null, - Settings.builder().put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build() + Settings.builder().put(SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, true).build(), + List.of() ) ); assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build())); @@ -70,7 +82,8 @@ public void accessHandlerForRestAdmin() { null, restApiAdminPrivilegesEvaluator, null, - Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build() + Settings.builder().put(SECURITY_RESTAPI_ADMIN_ENABLED, true).build(), + List.of() ) ); assertTrue(securityConfigApiAction.accessHandler(FakeRestRequest.builder().withMethod(RestRequest.Method.GET).build()));