diff --git a/.github/workflows/multi-node-test-workflow.yml b/.github/workflows/multi-node-test-workflow.yml index e0eb52ca7..172800a2d 100644 --- a/.github/workflows/multi-node-test-workflow.yml +++ b/.github/workflows/multi-node-test-workflow.yml @@ -23,26 +23,6 @@ jobs: uses: actions/setup-java@v1 with: java-version: ${{ matrix.java }} - # This step adds dependency, OpenSearch - - name: Checkout OpenSearch - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/OpenSearch' - path: OpenSearch - ref: '1.x' - - name: Build OpenSearch - working-directory: ./OpenSearch - run: ./gradlew publishToMavenLocal - # This step adds dependency, common-utils - - name: Checkout common-utils - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/common-utils' - path: common-utils - ref: 'main' - - name: Build common-utils - working-directory: ./common-utils - run: ./gradlew publishToMavenLocal -Dopensearch.version=1.1.0-SNAPSHOT # This step uses the checkout Github action: https://github.com/actions/checkout - name: Checkout Branch uses: actions/checkout@v2 @@ -52,7 +32,7 @@ jobs: with: java-version: 14 - name: Run integration tests with multi node config - run: ./gradlew integTest -PnumNodes=3 -Dopensearch.version=1.1.0-SNAPSHOT + run: ./gradlew integTest -PnumNodes=3 -Dopensearch.version=1.2.0-SNAPSHOT - name: Pull and Run Docker run: | plugin=`ls alerting/build/distributions/*.zip` @@ -98,3 +78,8 @@ jobs: else echo "Security plugin is NOT available skipping this run as tests without security have already been run" fi + + - name: Run Alerting Backwards Compatibility Tests + run: | + echo "Running backwards compatibility tests ..." + ./gradlew bwcTestSuite diff --git a/.github/workflows/push-notification-jar.yml b/.github/workflows/push-notification-jar.yml deleted file mode 100644 index 3b76e5854..000000000 --- a/.github/workflows/push-notification-jar.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Upload Notification Jar to Maven - -on: - push: - tags: - - v* -jobs: - upload-notification-jar: - runs-on: [ubuntu-16.04] - name: Upload Notification Jar to Maven - steps: - - name: Checkout Repo - uses: actions/checkout@v2 - - - name: Configure AWS CLI - uses: aws-actions/configure-aws-credentials@v1 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Setup Java - uses: actions/setup-java@v1 - with: - java-version: '14' - - - name: Upload Notification Jar to Maven - env: - passphrase: ${{ secrets.PASSPHRASE }} - run: | - cd .. - export JAVA14_HOME=$JAVA_HOME - aws s3 cp s3://opendistro-docs/github-actions/pgp-public-key . - aws s3 cp s3://opendistro-docs/github-actions/pgp-private-key . - - gpg --import pgp-public-key - gpg --allow-secret-key-import --import pgp-private-key - - mkdir /home/runner/.gradle - aws s3 cp s3://opendistro-docs/github-actions/gradle.properties /home/runner/.gradle/ - - cd alerting/notification - - ../gradlew publishShadowPublicationToSonatype-stagingRepository -Dcompiler.java=14 -Dbuild.snapshot=false -Djavax.net.ssl.trustStore=$JAVA_HOME/lib/security/cacerts diff --git a/.github/workflows/test-workflow.yml b/.github/workflows/test-workflow.yml index 6098fc7d0..fc7eda54e 100644 --- a/.github/workflows/test-workflow.yml +++ b/.github/workflows/test-workflow.yml @@ -29,30 +29,8 @@ jobs: with: java-version: ${{ matrix.java }} - # dependencies: OpenSearch - - name: Checkout OpenSearch - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/OpenSearch' - path: OpenSearch - ref: '1.x' - - name: Build OpenSearch - working-directory: ./OpenSearch - run: ./gradlew publishToMavenLocal - - # dependencies: common-utils - - name: Checkout common-utils - uses: actions/checkout@v2 - with: - repository: 'opensearch-project/common-utils' - path: common-utils - ref: 'main' - - name: Build common-utils - working-directory: ./common-utils - run: ./gradlew publishToMavenLocal -Dopensearch.version=1.1.0-SNAPSHOT - - name: Build and run with Gradle - run: ./gradlew build -Dopensearch.version=1.1.0-SNAPSHOT + run: ./gradlew build -Dopensearch.version=1.2.0-SNAPSHOT # - name: Create Artifact Path # run: | @@ -71,4 +49,4 @@ jobs: # path: alerting-artifacts # Publish to local maven - name: Publish to Maven Local - run: ./gradlew publishToMavenLocal -Dopensearch.version=1.1.0-SNAPSHOT + run: ./gradlew publishToMavenLocal -Dopensearch.version=1.2.0-SNAPSHOT diff --git a/DEVELOPER_GUIDE.md b/DEVELOPER_GUIDE.md index 93f36331b..282d23e93 100644 --- a/DEVELOPER_GUIDE.md +++ b/DEVELOPER_GUIDE.md @@ -44,6 +44,10 @@ Currently we just put RCF jar in lib as dependency. Plan to publish to Maven and 6. `./gradlew :alerting:integTest -Dtests.class="*MonitorRunnerIT"` runs a single integ test class 7. `./gradlew :alerting:integTest -Dtests.method="test execute monitor with dryrun"` runs a single integ test method (remember to quote the test method name if it contains spaces). +8. `./gradlew alertingBwcCluster#mixedClusterTask` launches a cluster with three nodes of bwc version of OpenSearch with alerting and tests backwards compatibility by upgrading one of the nodes with the current version of OpenSearch with alerting, creating a mixed cluster. +9. `./gradlew alertingBwcCluster#rollingUpgradeClusterTask` launches a cluster with three nodes of bwc version of OpenSearch with alerting and tests backwards compatibility by performing rolling upgrade of each node with the current version of OpenSearch with alerting. +10. `./gradlew alertingBwcCluster#fullRestartClusterTask` launches a cluster with three nodes of bwc version of OpenSearch with alerting and tests backwards compatibility by performing a full restart on the cluster upgrading all the nodes with the current version of OpenSearch with alerting. +11. `./gradlew bwcTestSuite` runs all the above bwc tests combined. When launching a cluster using one of the above commands, logs are placed in `alerting/build/testclusters/integTest-0/logs/`. Though the logs are teed to the console, in practices it's best to check the actual log file. diff --git a/README.md b/README.md index 9cd2b2c4a..91e5fa1c6 100644 --- a/README.md +++ b/README.md @@ -58,4 +58,4 @@ This project is licensed under the [Apache v2.0 License](LICENSE.txt). ## Copyright - Copyright 2020-2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file +Copyright OpenSearch Contributors. See [NOTICE](NOTICE.txt) for details. \ No newline at end of file diff --git a/alerting/build.gradle b/alerting/build.gradle index 1f31293c0..761304b1e 100644 --- a/alerting/build.gradle +++ b/alerting/build.gradle @@ -24,7 +24,9 @@ * permissions and limitations under the License. */ +import java.util.concurrent.Callable import org.opensearch.gradle.test.RestIntegTestTask +import org.opensearch.gradle.testclusters.StandaloneRestIntegTestTask apply plugin: 'java' apply plugin: 'idea' @@ -86,7 +88,7 @@ dependencies { implementation "com.github.seancfoley:ipaddress:5.3.3" testImplementation "org.jetbrains.kotlin:kotlin-test:${kotlin_version}" - testCompile "org.mockito:mockito-core:2.23.0" + testCompile "org.mockito:mockito-core:3.12.4" } javadoc.enabled = false // turn off javadoc as it barfs on Kotlin code @@ -164,6 +166,151 @@ integTest { 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.alerting.bwc.*IT" + } + } +} + +String bwcVersion = "1.13.1.0" +String baseName = "alertingBwcCluster" +String bwcFilePath = "src/test/resources/bwc" + +2.times {i -> + testClusters { + "${baseName}$i" { + testDistribution = "ARCHIVE" + versions = ["7.10.2","1.2.0-SNAPSHOT"] + numberOfNodes = 3 + plugin(provider(new Callable(){ + @Override + RegularFile call() throws Exception { + return new RegularFile() { + @Override + File getAsFile() { + return fileTree(bwcFilePath + "/alerting/" + bwcVersion).getSingleFile() + } + } + } + })) + setting 'path.repo', "${buildDir}/cluster/shared/repo/${baseName}" + setting 'http.content_type.required', 'true' + } + } +} + +List> plugins = [] + +// Ensure the artifact for the current project version is available to be used for the bwc tests +task prepareBwcTests { + dependsOn bundle + doLast { + plugins = [ + project.getObjects().fileProperty().value(bundle.getArchiveFile()) + ] + } +} + +// Create two test clusters with 3 nodes of the old version +2.times {i -> + task "${baseName}#oldVersionClusterTask$i"(type: StandaloneRestIntegTestTask) { + dependsOn 'prepareBwcTests' + useCluster testClusters."${baseName}$i" + filter { + includeTestsMatching "org.opensearch.alerting.bwc.*IT" + } + systemProperty 'tests.rest.bwcsuite', 'old_cluster' + systemProperty 'tests.rest.bwcsuite_round', 'old' + systemProperty 'tests.plugin_bwc_version', bwcVersion + nonInputProperties.systemProperty('tests.rest.cluster', "${-> testClusters."${baseName}$i".allHttpSocketURI.join(",")}") + nonInputProperties.systemProperty('tests.clustername', "${-> testClusters."${baseName}$i".getName()}") + } +} + +// Upgrade one node of the old cluster to new OpenSearch version with upgraded plugin version. +// This results in a mixed cluster with 2 nodes on the old version and 1 upgraded node. +// This is also used as a one third upgraded cluster for a rolling upgrade. +task "${baseName}#mixedClusterTask"(type: StandaloneRestIntegTestTask) { + useCluster testClusters."${baseName}0" + dependsOn "${baseName}#oldVersionClusterTask0" + doFirst { + testClusters."${baseName}0".upgradeNodeAndPluginToNextVersion(plugins) + } + filter { + includeTestsMatching "org.opensearch.alerting.bwc.*IT" + } + systemProperty 'tests.rest.bwcsuite', 'mixed_cluster' + systemProperty 'tests.rest.bwcsuite_round', 'first' + systemProperty 'tests.plugin_bwc_version', bwcVersion + nonInputProperties.systemProperty('tests.rest.cluster', "${-> testClusters."${baseName}0".allHttpSocketURI.join(",")}") + nonInputProperties.systemProperty('tests.clustername', "${-> testClusters."${baseName}0".getName()}") +} + +// Upgrade the second node to new OpenSearch version with upgraded plugin version after the first node is upgraded. +// This results in a mixed cluster with 1 node on the old version and 2 upgraded nodes. +// This is used for rolling upgrade. +task "${baseName}#twoThirdsUpgradedClusterTask"(type: StandaloneRestIntegTestTask) { + dependsOn "${baseName}#mixedClusterTask" + useCluster testClusters."${baseName}0" + doFirst { + testClusters."${baseName}0".upgradeNodeAndPluginToNextVersion(plugins) + } + filter { + includeTestsMatching "org.opensearch.alerting.bwc.*IT" + } + systemProperty 'tests.rest.bwcsuite', 'mixed_cluster' + systemProperty 'tests.rest.bwcsuite_round', 'second' + systemProperty 'tests.plugin_bwc_version', bwcVersion + nonInputProperties.systemProperty('tests.rest.cluster', "${-> testClusters."${baseName}0".allHttpSocketURI.join(",")}") + nonInputProperties.systemProperty('tests.clustername', "${-> testClusters."${baseName}0".getName()}") +} + +// Upgrade the third node to new OpenSearch version with upgraded plugin version after the second node is upgraded. +// This results in a fully upgraded cluster. +// This is used for rolling upgrade. +task "${baseName}#rollingUpgradeClusterTask"(type: StandaloneRestIntegTestTask) { + dependsOn "${baseName}#twoThirdsUpgradedClusterTask" + useCluster testClusters."${baseName}0" + doFirst { + testClusters."${baseName}0".upgradeNodeAndPluginToNextVersion(plugins) + } + filter { + includeTestsMatching "org.opensearch.alerting.bwc.*IT" + } + mustRunAfter "${baseName}#mixedClusterTask" + systemProperty 'tests.rest.bwcsuite', 'mixed_cluster' + systemProperty 'tests.rest.bwcsuite_round', 'third' + systemProperty 'tests.plugin_bwc_version', bwcVersion + nonInputProperties.systemProperty('tests.rest.cluster', "${-> testClusters."${baseName}0".allHttpSocketURI.join(",")}") + nonInputProperties.systemProperty('tests.clustername', "${-> testClusters."${baseName}0".getName()}") +} + +// Upgrade all the nodes of the old cluster to new OpenSearch version with upgraded plugin version +// at the same time resulting in a fully upgraded cluster. +task "${baseName}#fullRestartClusterTask"(type: StandaloneRestIntegTestTask) { + dependsOn "${baseName}#oldVersionClusterTask1" + useCluster testClusters."${baseName}1" + doFirst { + testClusters."${baseName}1".upgradeAllNodesAndPluginsToNextVersion(plugins) + } + filter { + includeTestsMatching "org.opensearch.alerting.bwc.*IT" + } + systemProperty 'tests.rest.bwcsuite', 'upgraded_cluster' + systemProperty 'tests.plugin_bwc_version', bwcVersion + nonInputProperties.systemProperty('tests.rest.cluster', "${-> testClusters."${baseName}1".allHttpSocketURI.join(",")}") + nonInputProperties.systemProperty('tests.clustername', "${-> testClusters."${baseName}1".getName()}") +} + +// A bwc test suite which runs all the bwc tasks combined +task bwcTestSuite(type: StandaloneRestIntegTestTask) { + exclude '**/*Test*' + exclude '**/*IT*' + dependsOn tasks.named("${baseName}#mixedClusterTask") + dependsOn tasks.named("${baseName}#rollingUpgradeClusterTask") + dependsOn tasks.named("${baseName}#fullRestartClusterTask") } run { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt new file mode 100644 index 000000000..4ad7be890 --- /dev/null +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt @@ -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.alerting.transport + +import org.apache.logging.log4j.LogManager +import org.opensearch.OpenSearchStatusException +import org.opensearch.action.ActionListener +import org.opensearch.alerting.settings.AlertingSettings +import org.opensearch.alerting.util.AlertingException +import org.opensearch.client.Client +import org.opensearch.cluster.service.ClusterService +import org.opensearch.commons.ConfigConstants +import org.opensearch.commons.authuser.User +import org.opensearch.rest.RestStatus + +private val log = LogManager.getLogger(SecureTransportAction::class.java) + +/** + * TransportActon classes extend this interface to add filter-by-backend-roles functionality. + * + * 1. If filterBy is enabled + * a) Don't allow to create monitor/ destination (throw error) if the logged-on user has no backend roles configured. + * + * 2. If filterBy is enabled & monitors are created when filterBy is disabled: + * a) If backend_roles are saved with config, results will get filtered and data is shown + * b) If backend_roles are not saved with monitor config, results will get filtered and no monitors + * will be displayed. + * c) Users can edit and save the monitors to associate their backend_roles. + * + * 3. If filterBy is enabled & monitors are created by older version: + * a) No User details are present on monitor. + * b) No monitors will be displayed. + * c) Users can edit and save the monitors to associate their backend_roles. + */ +interface SecureTransportAction { + + var filterByEnabled: Boolean + + fun listenFilterBySettingChange(clusterService: ClusterService) { + clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + } + + fun readUserFromThreadContext(client: Client): User? { + val userStr = client.threadPool().threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) + log.debug("User and roles string from thread context: $userStr") + return User.parse(userStr) + } + + fun doFilterForUser(user: User?): Boolean { + log.debug("Is filterByEnabled: $filterByEnabled ; Is admin user: ${isAdmin(user)}") + return if (isAdmin(user)) { + false + } else { + filterByEnabled + } + } + + /** + * 'all_access' role users are treated as admins. + */ + private fun isAdmin(user: User?): Boolean { + return when { + user == null -> { + false + } + user.roles?.isNullOrEmpty() == true -> { + false + } + else -> { + user.roles?.contains("all_access") == true + } + } + } + + fun validateUserBackendRoles(user: User?, actionListener: ActionListener): Boolean { + if (filterByEnabled) { + if (user == null) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException( + "Filter by user backend roles is enabled with security disabled.", RestStatus.FORBIDDEN + ) + ) + ) + return false + } else if (user.backendRoles.isNullOrEmpty()) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException("User doesn't have backend roles configured. Contact administrator", RestStatus.FORBIDDEN) + ) + ) + return false + } + } + return true + } + + /** + * If FilterBy is enabled, this function verifies that the requester user has FilterBy permissions to access + * the resource. If FilterBy is disabled, we will assume the user has permissions and return true. + * + * This check will later to moved to the security plugin. + */ + fun checkUserPermissionsWithResource( + requesterUser: User?, + resourceUser: User?, + actionListener: ActionListener, + resourceType: String, + resourceId: String + ): Boolean { + + if (!filterByEnabled) return true + + val resourceBackendRoles = resourceUser?.backendRoles + val requesterBackendRoles = requesterUser?.backendRoles + + if (resourceBackendRoles == null || requesterBackendRoles == null || resourceBackendRoles.intersect(requesterBackendRoles).isEmpty()) { + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException( + "Do not have permissions to resource, $resourceType, with id, $resourceId", + RestStatus.FORBIDDEN + ) + ) + ) + return false + } + return true + } +} diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteDestinationAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteDestinationAction.kt index 2537e0ffa..2657363c0 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteDestinationAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteDestinationAction.kt @@ -41,8 +41,6 @@ import org.opensearch.alerting.core.model.ScheduledJob import org.opensearch.alerting.model.destination.Destination import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.util.AlertingException -import org.opensearch.alerting.util.checkFilterByUserBackendRoles -import org.opensearch.alerting.util.checkUserFilterByPermissions import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject @@ -53,7 +51,6 @@ import org.opensearch.common.xcontent.XContentFactory import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParserUtils import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.ConfigConstants import org.opensearch.commons.authuser.User import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task @@ -71,22 +68,21 @@ class TransportDeleteDestinationAction @Inject constructor( val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( DeleteDestinationAction.NAME, transportService, actionFilters, ::DeleteDestinationRequest -) { +), + SecureTransportAction { - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, request: DeleteDestinationRequest, actionListener: ActionListener) { - val userStr = client.threadPool().threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) + val user = readUserFromThreadContext(client) val deleteRequest = DeleteRequest(ScheduledJob.SCHEDULED_JOBS_INDEX, request.destinationId) .setRefreshPolicy(request.refreshPolicy) - if (!checkFilterByUserBackendRoles(filterByEnabled, user, actionListener)) { + if (!validateUserBackendRoles(user, actionListener)) { return } client.threadPool().threadContext.stashContext().use { @@ -106,7 +102,7 @@ class TransportDeleteDestinationAction @Inject constructor( if (user == null) { // Security is disabled, so we can delete the destination without issues deleteDestination() - } else if (!filterByEnabled) { + } else if (!doFilterForUser(user)) { // security is enabled and filterby is disabled. deleteDestination() } else { @@ -153,7 +149,7 @@ class TransportDeleteDestinationAction @Inject constructor( } private fun onGetResponse(destination: Destination) { - if (!checkUserFilterByPermissions(filterByEnabled, user, destination.user, actionListener, "destination", destinationId)) { + if (!checkUserPermissionsWithResource(user, destination.user, actionListener, "destination", destinationId)) { return } else { deleteDestination() diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt index fff81b2c6..9acbbd266 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportDeleteMonitorAction.kt @@ -41,8 +41,6 @@ import org.opensearch.alerting.core.model.ScheduledJob import org.opensearch.alerting.model.Monitor import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.util.AlertingException -import org.opensearch.alerting.util.checkFilterByUserBackendRoles -import org.opensearch.alerting.util.checkUserFilterByPermissions import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject @@ -51,7 +49,6 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.ConfigConstants import org.opensearch.commons.authuser.User import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task @@ -69,22 +66,21 @@ class TransportDeleteMonitorAction @Inject constructor( val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( DeleteMonitorAction.NAME, transportService, actionFilters, ::DeleteMonitorRequest -) { +), + SecureTransportAction { - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, request: DeleteMonitorRequest, actionListener: ActionListener) { - val userStr = client.threadPool().threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) + val user = readUserFromThreadContext(client) val deleteRequest = DeleteRequest(ScheduledJob.SCHEDULED_JOBS_INDEX, request.monitorId) .setRefreshPolicy(request.refreshPolicy) - if (!checkFilterByUserBackendRoles(filterByEnabled, user, actionListener)) { + if (!validateUserBackendRoles(user, actionListener)) { return } client.threadPool().threadContext.stashContext().use { @@ -104,7 +100,7 @@ class TransportDeleteMonitorAction @Inject constructor( if (user == null) { // Security is disabled, so we can delete the destination without issues deleteMonitor() - } else if (!filterByEnabled) { + } else if (!doFilterForUser(user)) { // security is enabled and filterby is disabled. deleteMonitor() } else { @@ -145,7 +141,7 @@ class TransportDeleteMonitorAction @Inject constructor( } private fun onGetResponse(monitor: Monitor) { - if (!checkUserFilterByPermissions(filterByEnabled, user, monitor.user, actionListener, "monitor", monitorId)) { + if (!checkUserPermissionsWithResource(user, monitor.user, actionListener, "monitor", monitorId)) { return } else { deleteMonitor() diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt index f5da20102..6a10bd528 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetAlertsAction.kt @@ -50,7 +50,6 @@ import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParserUtils import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.ConfigConstants import org.opensearch.commons.authuser.User import org.opensearch.index.query.Operator import org.opensearch.index.query.QueryBuilders @@ -72,12 +71,13 @@ class TransportGetAlertsAction @Inject constructor( val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( GetAlertsAction.NAME, transportService, actionFilters, ::GetAlertsRequest -) { +), + SecureTransportAction { - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute( @@ -85,11 +85,7 @@ class TransportGetAlertsAction @Inject constructor( getAlertsRequest: GetAlertsRequest, actionListener: ActionListener ) { - val userStr = client.threadPool().threadContext.getTransient( - ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT - ) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) + val user = readUserFromThreadContext(client) val tableProp = getAlertsRequest.table val sortBuilder = SortBuilders @@ -143,7 +139,7 @@ class TransportGetAlertsAction @Inject constructor( if (user == null) { // user is null when: 1/ security is disabled. 2/when user is super-admin. search(searchSourceBuilder, actionListener) - } else if (!filterByEnabled) { + } else if (!doFilterForUser(user)) { // security is enabled and filterby is disabled. search(searchSourceBuilder, actionListener) } else { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt index 27683b928..b74df5508 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetDestinationsAction.kt @@ -51,7 +51,6 @@ import org.opensearch.common.xcontent.XContentFactory import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParserUtils import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.ConfigConstants import org.opensearch.commons.authuser.User import org.opensearch.index.query.Operator import org.opensearch.index.query.QueryBuilders @@ -75,12 +74,13 @@ class TransportGetDestinationsAction @Inject constructor( val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction ( GetDestinationsAction.NAME, transportService, actionFilters, ::GetDestinationsRequest -) { +), + SecureTransportAction { - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute( @@ -88,12 +88,7 @@ class TransportGetDestinationsAction @Inject constructor( getDestinationsRequest: GetDestinationsRequest, actionListener: ActionListener ) { - val userStr = client.threadPool().threadContext.getTransient( - ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT - ) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) - + val user = readUserFromThreadContext(client) val tableProp = getDestinationsRequest.table val sortBuilder = SortBuilders @@ -144,7 +139,7 @@ class TransportGetDestinationsAction @Inject constructor( if (user == null) { // user is null when: 1/ security is disabled. 2/when user is super-admin. search(searchSourceBuilder, actionListener) - } else if (!filterByEnabled) { + } else if (!doFilterForUser(user)) { // security is enabled and filterby is disabled. search(searchSourceBuilder, actionListener) } else { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt index 8de0c5a08..9eb250c0f 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportGetMonitorAction.kt @@ -40,8 +40,6 @@ import org.opensearch.alerting.core.model.ScheduledJob import org.opensearch.alerting.model.Monitor import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.util.AlertingException -import org.opensearch.alerting.util.checkFilterByUserBackendRoles -import org.opensearch.alerting.util.checkUserFilterByPermissions import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject @@ -50,8 +48,6 @@ import org.opensearch.common.xcontent.LoggingDeprecationHandler import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.ConfigConstants -import org.opensearch.commons.authuser.User import org.opensearch.rest.RestStatus import org.opensearch.tasks.Task import org.opensearch.transport.TransportService @@ -67,24 +63,23 @@ class TransportGetMonitorAction @Inject constructor( settings: Settings ) : HandledTransportAction ( GetMonitorAction.NAME, transportService, actionFilters, ::GetMonitorRequest -) { +), + SecureTransportAction { - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, getMonitorRequest: GetMonitorRequest, actionListener: ActionListener) { - val userStr = client.threadPool().threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) + val user = readUserFromThreadContext(client) val getRequest = GetRequest(ScheduledJob.SCHEDULED_JOBS_INDEX, getMonitorRequest.monitorId) .version(getMonitorRequest.version) .fetchSourceContext(getMonitorRequest.srcContext) - if (!checkFilterByUserBackendRoles(filterByEnabled, user, actionListener)) { + if (!validateUserBackendRoles(user, actionListener)) { return } @@ -115,8 +110,7 @@ class TransportGetMonitorAction @Inject constructor( monitor = ScheduledJob.parse(xcp, response.id, response.version) as Monitor // security is enabled and filterby is enabled - if (!checkUserFilterByPermissions( - filterByEnabled, + if (!checkUserPermissionsWithResource( user, monitor?.user, actionListener, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexDestinationAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexDestinationAction.kt index fd7f72fec..4a80acd4d 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexDestinationAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexDestinationAction.kt @@ -32,8 +32,6 @@ import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.settings.DestinationSettings import org.opensearch.alerting.util.AlertingException import org.opensearch.alerting.util.IndexUtils -import org.opensearch.alerting.util.checkFilterByUserBackendRoles -import org.opensearch.alerting.util.checkUserFilterByPermissions import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject @@ -46,7 +44,6 @@ import org.opensearch.common.xcontent.XContentFactory.jsonBuilder import org.opensearch.common.xcontent.XContentParser import org.opensearch.common.xcontent.XContentParserUtils import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.ConfigConstants import org.opensearch.commons.authuser.User import org.opensearch.rest.RestRequest import org.opensearch.rest.RestStatus @@ -66,24 +63,23 @@ class TransportIndexDestinationAction @Inject constructor( val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( IndexDestinationAction.NAME, transportService, actionFilters, ::IndexDestinationRequest -) { +), + SecureTransportAction { @Volatile private var indexTimeout = AlertingSettings.INDEX_TIMEOUT.get(settings) @Volatile private var allowList = DestinationSettings.ALLOW_LIST.get(settings) - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.INDEX_TIMEOUT) { indexTimeout = it } clusterService.clusterSettings.addSettingsUpdateConsumer(DestinationSettings.ALLOW_LIST) { allowList = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, request: IndexDestinationRequest, actionListener: ActionListener) { - val userStr = client.threadPool().threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) + val user = readUserFromThreadContext(client) - if (!checkFilterByUserBackendRoles(filterByEnabled, user, actionListener)) { + if (!validateUserBackendRoles(user, actionListener)) { return } client.threadPool().threadContext.stashContext().use { @@ -267,8 +263,7 @@ class TransportIndexDestinationAction @Inject constructor( } private fun onGetResponse(destination: Destination) { - if (!checkUserFilterByPermissions( - filterByEnabled, + if (!checkUserPermissionsWithResource( user, destination.user, actionListener, diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt index 3602534f7..c1f9ccdcc 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt @@ -51,7 +51,6 @@ import org.opensearch.alerting.core.model.SearchInput import org.opensearch.alerting.model.Monitor import org.opensearch.alerting.settings.AlertingSettings import org.opensearch.alerting.settings.AlertingSettings.Companion.ALERTING_MAX_MONITORS -import org.opensearch.alerting.settings.AlertingSettings.Companion.FILTER_BY_BACKEND_ROLES import org.opensearch.alerting.settings.AlertingSettings.Companion.INDEX_TIMEOUT import org.opensearch.alerting.settings.AlertingSettings.Companion.MAX_ACTION_THROTTLE_VALUE import org.opensearch.alerting.settings.AlertingSettings.Companion.REQUEST_TIMEOUT @@ -59,8 +58,6 @@ import org.opensearch.alerting.settings.DestinationSettings.Companion.ALLOW_LIST import org.opensearch.alerting.util.AlertingException import org.opensearch.alerting.util.IndexUtils import org.opensearch.alerting.util.addUserBackendRolesFilter -import org.opensearch.alerting.util.checkFilterByUserBackendRoles -import org.opensearch.alerting.util.checkUserFilterByPermissions import org.opensearch.alerting.util.isADMonitor import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService @@ -73,7 +70,6 @@ import org.opensearch.common.xcontent.ToXContent import org.opensearch.common.xcontent.XContentFactory.jsonBuilder import org.opensearch.common.xcontent.XContentHelper import org.opensearch.common.xcontent.XContentType -import org.opensearch.commons.ConfigConstants import org.opensearch.commons.authuser.User import org.opensearch.index.query.QueryBuilders import org.opensearch.rest.RestRequest @@ -96,14 +92,15 @@ class TransportIndexMonitorAction @Inject constructor( val xContentRegistry: NamedXContentRegistry ) : HandledTransportAction( IndexMonitorAction.NAME, transportService, actionFilters, ::IndexMonitorRequest -) { +), + SecureTransportAction { @Volatile private var maxMonitors = ALERTING_MAX_MONITORS.get(settings) @Volatile private var requestTimeout = REQUEST_TIMEOUT.get(settings) @Volatile private var indexTimeout = INDEX_TIMEOUT.get(settings) @Volatile private var maxActionThrottle = MAX_ACTION_THROTTLE_VALUE.get(settings) @Volatile private var allowList = ALLOW_LIST.get(settings) - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) + @Volatile override var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { clusterService.clusterSettings.addSettingsUpdateConsumer(ALERTING_MAX_MONITORS) { maxMonitors = it } @@ -111,16 +108,13 @@ class TransportIndexMonitorAction @Inject constructor( clusterService.clusterSettings.addSettingsUpdateConsumer(INDEX_TIMEOUT) { indexTimeout = it } clusterService.clusterSettings.addSettingsUpdateConsumer(MAX_ACTION_THROTTLE_VALUE) { maxActionThrottle = it } clusterService.clusterSettings.addSettingsUpdateConsumer(ALLOW_LIST) { allowList = it } - clusterService.clusterSettings.addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, request: IndexMonitorRequest, actionListener: ActionListener) { + val user = readUserFromThreadContext(client) - val userStr = client.threadPool().threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) - - if (!checkFilterByUserBackendRoles(filterByEnabled, user, actionListener)) { + if (!validateUserBackendRoles(user, actionListener)) { return } @@ -458,7 +452,7 @@ class TransportIndexMonitorAction @Inject constructor( } private fun onGetResponse(currentMonitor: Monitor) { - if (!checkUserFilterByPermissions(filterByEnabled, user, currentMonitor.user, actionListener, "monitor", request.monitorId)) { + if (!checkUserPermissionsWithResource(user, currentMonitor.user, actionListener, "monitor", request.monitorId)) { return } diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt index 198203e0f..686ce4442 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportSearchMonitorAction.kt @@ -41,7 +41,6 @@ import org.opensearch.client.Client import org.opensearch.cluster.service.ClusterService import org.opensearch.common.inject.Inject import org.opensearch.common.settings.Settings -import org.opensearch.commons.ConfigConstants import org.opensearch.commons.authuser.User import org.opensearch.tasks.Task import org.opensearch.transport.TransportService @@ -56,18 +55,16 @@ class TransportSearchMonitorAction @Inject constructor( actionFilters: ActionFilters ) : HandledTransportAction( SearchMonitorAction.NAME, transportService, actionFilters, ::SearchMonitorRequest -) { - @Volatile private var filterByEnabled = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) - +), + SecureTransportAction { + @Volatile + override var filterByEnabled: Boolean = AlertingSettings.FILTER_BY_BACKEND_ROLES.get(settings) init { - clusterService.clusterSettings.addSettingsUpdateConsumer(AlertingSettings.FILTER_BY_BACKEND_ROLES) { filterByEnabled = it } + listenFilterBySettingChange(clusterService) } override fun doExecute(task: Task, searchMonitorRequest: SearchMonitorRequest, actionListener: ActionListener) { - val userStr = client.threadPool().threadContext.getTransient(ConfigConstants.OPENSEARCH_SECURITY_USER_INFO_THREAD_CONTEXT) - log.debug("User and roles string from thread context: $userStr") - val user: User? = User.parse(userStr) - + val user = readUserFromThreadContext(client) client.threadPool().threadContext.stashContext().use { resolve(searchMonitorRequest, actionListener, user) } @@ -77,7 +74,7 @@ class TransportSearchMonitorAction @Inject constructor( if (user == null) { // user header is null when: 1/ security is disabled. 2/when user is super-admin. search(searchMonitorRequest.searchRequest, actionListener) - } else if (!filterByEnabled) { + } else if (!doFilterForUser(user)) { // security is enabled and filterby is disabled. search(searchMonitorRequest.searchRequest, actionListener) } else { diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/util/AlertingUtils.kt b/alerting/src/main/kotlin/org/opensearch/alerting/util/AlertingUtils.kt index 61f4dd9e0..e5558ecaa 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/util/AlertingUtils.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/util/AlertingUtils.kt @@ -27,8 +27,6 @@ package org.opensearch.alerting.util import inet.ipaddr.IPAddressString -import org.opensearch.OpenSearchStatusException -import org.opensearch.action.ActionListener import org.opensearch.alerting.destination.message.BaseMessage import org.opensearch.alerting.model.AggregationResultBucket import org.opensearch.alerting.model.BucketLevelTriggerRunResult @@ -37,8 +35,6 @@ import org.opensearch.alerting.model.action.Action import org.opensearch.alerting.model.action.ActionExecutionPolicy import org.opensearch.alerting.model.destination.Destination import org.opensearch.alerting.settings.DestinationSettings -import org.opensearch.commons.authuser.User -import org.opensearch.rest.RestStatus /** * RFC 5322 compliant pattern matching: https://www.ietf.org/rfc/rfc5322.txt @@ -75,76 +71,6 @@ fun BaseMessage.isHostInDenylist(networks: List): Boolean { return false } -/** - 1. If filterBy is enabled - a) Don't allow to create monitor/ destination (throw error) if the logged-on user has no backend roles configured. - 2. If filterBy is enabled & monitors are created when filterBy is disabled: - a) If backend_roles are saved with config, results will get filtered and data is shown - b) If backend_roles are not saved with monitor config, results will get filtered and no monitors - will be displayed. - c) Users can edit and save the monitors to associate their backend_roles. - 3. If filterBy is enabled & monitors are created by older version: - a) No User details are present on monitor. - b) No monitors will be displayed. - c) Users can edit and save the monitors to associate their backend_roles. - */ -fun checkFilterByUserBackendRoles(filterByEnabled: Boolean, user: User?, actionListener: ActionListener): Boolean { - if (filterByEnabled) { - if (user == null) { - actionListener.onFailure( - AlertingException.wrap( - OpenSearchStatusException( - "Filter by user backend roles is not enabled with security disabled.", RestStatus.FORBIDDEN - ) - ) - ) - return false - } else if (user.backendRoles.isNullOrEmpty()) { - actionListener.onFailure( - AlertingException.wrap( - OpenSearchStatusException("User doesn't have backend roles configured. Contact administrator.", RestStatus.FORBIDDEN) - ) - ) - return false - } - } - return true -} - -/** - * If FilterBy is enabled, this function verifies that the requester user has FilterBy permissions to access - * the resource. If FilterBy is disabled, we will assume the user has permissions and return true. - * - * This check will later to moved to the security plugin. - */ -fun checkUserFilterByPermissions( - filterByEnabled: Boolean, - requesterUser: User?, - resourceUser: User?, - actionListener: ActionListener, - resourceType: String, - resourceId: String -): Boolean { - - if (!filterByEnabled) return true - - val resourceBackendRoles = resourceUser?.backendRoles - val requesterBackendRoles = requesterUser?.backendRoles - - if (resourceBackendRoles == null || requesterBackendRoles == null || resourceBackendRoles.intersect(requesterBackendRoles).isEmpty()) { - actionListener.onFailure( - AlertingException.wrap( - OpenSearchStatusException( - "Do not have permissions to resource, $resourceType, with id, $resourceId", - RestStatus.FORBIDDEN - ) - ) - ) - return false - } - return true -} - fun Monitor.isBucketLevelMonitor(): Boolean = this.monitorType == Monitor.MonitorType.BUCKET_LEVEL_MONITOR /** diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index 83a0efc11..b48db925e 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -85,9 +85,10 @@ import javax.management.remote.JMXServiceURL abstract class AlertingRestTestCase : ODFERestTestCase() { - private val isDebuggingTest = DisableOnDebug(null).isDebugging - private val isDebuggingRemoteCluster = System.getProperty("cluster.debug", "false")!!.toBoolean() - val numberOfNodes = System.getProperty("cluster.number_of_nodes", "1")!!.toInt() + protected val isDebuggingTest = DisableOnDebug(null).isDebugging + protected val isDebuggingRemoteCluster = System.getProperty("cluster.debug", "false")!!.toBoolean() + protected val numberOfNodes = System.getProperty("cluster.number_of_nodes", "1")!!.toInt() + protected val isMultiNode = numberOfNodes > 1 override fun xContentRegistry(): NamedXContentRegistry { return NamedXContentRegistry( diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/ODFERestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/ODFERestTestCase.kt index 5c4081b35..700ba8cf2 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/ODFERestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/ODFERestTestCase.kt @@ -78,9 +78,13 @@ abstract class ODFERestTestCase : OpenSearchRestTestCase() { return true } + open fun preserveODFEIndicesAfterTest(): Boolean = false + @Throws(IOException::class) @After open fun wipeAllODFEIndices() { + if (preserveODFEIndicesAfterTest()) return + val response = client().performRequest(Request("GET", "/_cat/indices?format=json&expand_wildcards=all")) val xContentType = XContentType.fromMediaTypeOrFormat(response.entity.contentType.value) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/bwc/AlertingBackwardsCompatibilityIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/bwc/AlertingBackwardsCompatibilityIT.kt new file mode 100644 index 000000000..3f8f431ac --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/bwc/AlertingBackwardsCompatibilityIT.kt @@ -0,0 +1,220 @@ +/* + * 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.alerting.bwc + +import org.apache.http.entity.ContentType.APPLICATION_JSON +import org.apache.http.entity.StringEntity +import org.opensearch.alerting.ALERTING_BASE_URI +import org.opensearch.alerting.AlertingRestTestCase +import org.opensearch.alerting.LEGACY_OPENDISTRO_ALERTING_BASE_URI +import org.opensearch.alerting.makeRequest +import org.opensearch.alerting.model.Monitor +import org.opensearch.common.settings.Settings +import org.opensearch.common.xcontent.XContentType +import org.opensearch.index.query.QueryBuilders +import org.opensearch.rest.RestStatus +import org.opensearch.search.builder.SearchSourceBuilder + +class AlertingBackwardsCompatibilityIT : AlertingRestTestCase() { + + companion object { + private val CLUSTER_TYPE = ClusterType.parse(System.getProperty("tests.rest.bwcsuite")) + private val CLUSTER_NAME = System.getProperty("tests.clustername") + } + + override fun preserveIndicesUponCompletion(): Boolean = true + + override fun preserveReposUponCompletion(): Boolean = true + + override fun preserveTemplatesUponCompletion(): Boolean = true + + override fun preserveODFEIndicesAfterTest(): Boolean = true + + override fun restClientSettings(): Settings { + return Settings.builder() + .put(super.restClientSettings()) + // increase the timeout here to 90 seconds to handle long waits for a green + // cluster health. the waits for green need to be longer than a minute to + // account for delayed shards + .put(CLIENT_SOCKET_TIMEOUT, "90s") + .build() + } + + @Throws(Exception::class) + @Suppress("UNCHECKED_CAST") + fun `test backwards compatibility`() { + val uri = getPluginUri() + val responseMap = getAsMap(uri)["nodes"] as Map> + for (response in responseMap.values) { + val plugins = response["plugins"] as List> + val pluginNames = plugins.map { plugin -> plugin["name"] }.toSet() + when (CLUSTER_TYPE) { + ClusterType.OLD -> { + assertTrue(pluginNames.contains("opendistro-alerting")) + createBasicMonitor() + } + ClusterType.MIXED -> { + assertTrue(pluginNames.contains("opensearch-alerting")) + verifyMonitorExists(LEGACY_OPENDISTRO_ALERTING_BASE_URI) + // Waiting a minute to ensure the Monitor ran again at least once before checking if the job is running + // on time + // TODO: Should probably change the next execution time of the Monitor manually instead since this inflates + // the test execution by a lot + Thread.sleep(60000) + // TODO: Need to move the base URI being used here into a constant and rename ALERTING_BASE_URI to + // MONITOR_BASE_URI + verifyMonitorStats("/_opendistro/_alerting") + } + ClusterType.UPGRADED -> { + assertTrue(pluginNames.contains("opensearch-alerting")) + verifyMonitorExists(ALERTING_BASE_URI) + Thread.sleep(60000) + verifyMonitorStats("/_plugins/_alerting") + } + } + break + } + } + + private enum class ClusterType { + OLD, + MIXED, + UPGRADED; + + companion object { + fun parse(value: String): ClusterType { + return when (value) { + "old_cluster" -> OLD + "mixed_cluster" -> MIXED + "upgraded_cluster" -> UPGRADED + else -> throw AssertionError("Unknown cluster type: $value") + } + } + } + } + + private fun getPluginUri(): String { + return when (CLUSTER_TYPE) { + ClusterType.OLD -> "_nodes/$CLUSTER_NAME-0/plugins" + ClusterType.MIXED -> { + when (System.getProperty("tests.rest.bwcsuite_round")) { + "second" -> "_nodes/$CLUSTER_NAME-1/plugins" + "third" -> "_nodes/$CLUSTER_NAME-2/plugins" + else -> "_nodes/$CLUSTER_NAME-0/plugins" + } + } + ClusterType.UPGRADED -> "_nodes/plugins" + } + } + + @Throws(Exception::class) + private fun createBasicMonitor() { + val indexName = "test_bwc_index" + val legacyMonitorString = """ + { + "type": "monitor", + "name": "test_bwc_monitor", + "enabled": true, + "schedule": { + "period": { + "interval": 1, + "unit": "MINUTES" + } + }, + "inputs": [ + { + "search": { + "indices": [ + "$indexName" + ], + "query": { + "size": 0, + "query": { + "match_all": {} + } + } + } + } + ], + "triggers": [ + { + "name": "abc", + "severity": "1", + "condition": { + "script": { + "source": "ctx.results[0].hits.total.value > 100000", + "lang": "painless" + } + }, + "actions": [] + } + ] + } + """.trimIndent() + createIndex(indexName, Settings.EMPTY) + + val createResponse = client().makeRequest( + method = "POST", + endpoint = "$LEGACY_OPENDISTRO_ALERTING_BASE_URI?refresh=true", + params = emptyMap(), + entity = StringEntity(legacyMonitorString, APPLICATION_JSON) + ) + + assertEquals("Create monitor failed", RestStatus.CREATED, createResponse.restStatus()) + val responseBody = createResponse.asMap() + val createdId = responseBody["_id"] as String + val createdVersion = responseBody["_version"] as Int + assertNotEquals("Create monitor response is missing id", Monitor.NO_ID, createdId) + assertTrue("Create monitor reponse has incorrect version", createdVersion > 0) + } + + @Throws(Exception::class) + @Suppress("UNCHECKED_CAST") + private fun verifyMonitorExists(uri: String) { + val search = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()).toString() + val searchResponse = client().makeRequest( + "GET", + "$uri/_search", + emptyMap(), + StringEntity(search, APPLICATION_JSON) + ) + assertEquals("Search monitor failed", RestStatus.OK, searchResponse.restStatus()) + val xcp = createParser(XContentType.JSON.xContent(), searchResponse.entity.content) + val hits = xcp.map()["hits"]!! as Map> + val numberDocsFound = hits["total"]?.get("value") + assertEquals("Unexpected number of Monitors returned", 1, numberDocsFound) + } + + @Throws(Exception::class) + @Suppress("UNCHECKED_CAST") + /** + * Monitor stats will check if the Monitor scheduled job is running on time but does not necessarily mean that the + * Monitor execution itself did not fail. + */ + private fun verifyMonitorStats(uri: String) { + val statsResponse = client().makeRequest( + "GET", + "$uri/stats", + emptyMap() + ) + assertEquals("Monitor stats failed", RestStatus.OK, statsResponse.restStatus()) + val xcp = createParser(XContentType.JSON.xContent(), statsResponse.entity.content) + val responseMap = xcp.map() + val nodesCount = responseMap["_nodes"]!! as Map + val totalNodes = nodesCount["total"] + val successfulNodes = nodesCount["successful"] + val nodesOnSchedule = responseMap["nodes_on_schedule"]!! + assertEquals("Incorrect number of total nodes", 3, totalNodes) + assertEquals("Some nodes in stats response failed", totalNodes, successfulNodes) + assertEquals("Not all nodes are on schedule", totalNodes, nodesOnSchedule) + } +} diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt index d613d9719..e167e76c7 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt @@ -31,7 +31,10 @@ import org.opensearch.alerting.randomTemplateScript import org.opensearch.client.Response import org.opensearch.client.ResponseException import org.opensearch.client.RestClient +import org.opensearch.common.xcontent.LoggingDeprecationHandler +import org.opensearch.common.xcontent.NamedXContentRegistry import org.opensearch.common.xcontent.XContentType +import org.opensearch.common.xcontent.json.JsonXContent import org.opensearch.commons.authuser.User import org.opensearch.commons.rest.SecureRestClientBuilder import org.opensearch.index.query.QueryBuilders @@ -476,4 +479,46 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { deleteRoleMapping("alerting_full_access") } } + + fun `test admin all access with enable filter by`() { + if (!securityEnabled()) + return + + enableFilterBy() + createUserWithTestData(user, "hr_data", "hr_role", "HR") + createUserRolesMapping("alerting_full_access", arrayOf(user)) + try { + // randomMonitor has a dummy user, api ignores the User passed as part of monitor, it picks user info from the logged-in user. + val monitor = randomQueryLevelMonitor().copy( + inputs = listOf( + SearchInput( + indices = listOf("hr_data"), query = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()) + ) + ) + ) + + val createResponse = userClient?.makeRequest("POST", ALERTING_BASE_URI, emptyMap(), monitor.toHttpEntity()) + assertEquals("Create monitor failed", RestStatus.CREATED, createResponse?.restStatus()) + val monitorJson = JsonXContent.jsonXContent.createParser( + NamedXContentRegistry.EMPTY, LoggingDeprecationHandler.INSTANCE, + createResponse?.entity?.content + ).map() + + val search = SearchSourceBuilder().query(QueryBuilders.termQuery("_id", monitorJson["_id"])).toString() + + // search as "admin" - must get 1 docs + val adminSearchResponse = client().makeRequest( + "POST", + "$ALERTING_BASE_URI/_search", + emptyMap(), + NStringEntity(search, ContentType.APPLICATION_JSON) + ) + assertEquals("Search monitor failed", RestStatus.OK, adminSearchResponse.restStatus()) + assertEquals("Monitor not found during search", 1, getDocs(adminSearchResponse)) + } finally { + deleteRoleMapping("hr_role") + deleteRole("hr_role") + deleteRoleMapping("alerting_full_access") + } + } } diff --git a/alerting/src/test/resources/bwc/alerting/1.13.1.0/opendistro-alerting-1.13.1.0.zip b/alerting/src/test/resources/bwc/alerting/1.13.1.0/opendistro-alerting-1.13.1.0.zip new file mode 100644 index 000000000..37f0e4f83 Binary files /dev/null and b/alerting/src/test/resources/bwc/alerting/1.13.1.0/opendistro-alerting-1.13.1.0.zip differ diff --git a/build-tools/repositories.gradle b/build-tools/repositories.gradle index 2421552ca..67e5e47ed 100644 --- a/build-tools/repositories.gradle +++ b/build-tools/repositories.gradle @@ -26,6 +26,7 @@ repositories { mavenLocal() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } mavenCentral() jcenter() } \ No newline at end of file diff --git a/build.gradle b/build.gradle index c92b6430d..ff859d31c 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ buildscript { apply from: 'build-tools/repositories.gradle' ext { - opensearch_version = System.getProperty("opensearch.version", "1.1.0-SNAPSHOT") + opensearch_version = System.getProperty("opensearch.version", "1.2.0-SNAPSHOT") // 1.0.0 -> 1.0.0.0, and 1.0.0-SNAPSHOT -> 1.0.0.0-SNAPSHOT opensearch_build = opensearch_version.replaceAll(/(\.\d)([^\d]*)$/, '$1.0$2') common_utils_version = System.getProperty("common_utils.version", opensearch_build) @@ -37,6 +37,7 @@ buildscript { repositories { mavenLocal() + maven { url "https://aws.oss.sonatype.org/content/repositories/snapshots" } mavenCentral() maven { url "https://plugins.gradle.org/m2/" } jcenter() diff --git a/notification/build.gradle b/notification/build.gradle index 5fb42ccaf..8477d7d33 100644 --- a/notification/build.gradle +++ b/notification/build.gradle @@ -58,6 +58,12 @@ task javadocJar(type: Jar) { } publishing { + repositories { + maven { + name = 'staging' + url = "${rootProject.buildDir}/local-staging-repo" + } + } publications { shadow(MavenPublication) { project.shadow.component(it) @@ -94,23 +100,7 @@ publishing { } } - repositories { - maven { - name = "sonatype-staging" - url "https://aws.oss.sonatype.org/service/local/staging/deploy/maven2" - credentials { - username project.hasProperty('ossrhUsername') ? project.property('ossrhUsername') : '' - password project.hasProperty('ossrhPassword') ? project.property('ossrhPassword') : '' - } - } - } - // TODO - enabled debug logging for the time being, remove this eventually gradle.startParameter.setShowStacktrace(ShowStacktrace.ALWAYS) gradle.startParameter.setLogLevel(LogLevel.DEBUG) - - signing { - required { gradle.taskGraph.hasTask("publishShadowPublicationToSonatype-stagingRepository") } - sign publishing.publications.shadow - } } diff --git a/release-notes/opensearch-alerting.release-notes-1.2.0.0.md b/release-notes/opensearch-alerting.release-notes-1.2.0.0.md new file mode 100644 index 000000000..5cfe0b553 --- /dev/null +++ b/release-notes/opensearch-alerting.release-notes-1.2.0.0.md @@ -0,0 +1,30 @@ +## Version 1.2.0.0 2021-11-05 + +Compatible with OpenSearch 1.2.0 + +### Enhancements + +* Admin Users must be able to access all monitors #139 ([#220](https://github.com/opensearch-project/alerting/pull/220)) +* Add valid search filters. ([#194](https://github.com/opensearch-project/alerting/pull/194)) + +### Bug Fixes + +* Fixed a bug that was preventing the AcknowledgeAlerts API from acknowledging more than 10 alerts at once. ([#205](https://github.com/opensearch-project/alerting/pull/205)) +* Remove user from the responses ([#201](https://github.com/opensearch-project/alerting/pull/201)) + +### Infrastructure + +* Update build to use public Maven repo ([#184](https://github.com/opensearch-project/alerting/pull/184)) +* Publish notification JARs checksums. ([#196](https://github.com/opensearch-project/alerting/pull/196)) +* Updates testCompile mockito version to match OpenSearch changes ([#204](https://github.com/opensearch-project/alerting/pull/204)) +* Update maven publication to include cksums. ([#224](https://github.com/opensearch-project/alerting/pull/224)) +* Updates alerting version to 1.2 ([#192](https://github.com/opensearch-project/alerting/pull/192)) + +### Documentation + +* Add release notes for 1.2.0.0 release ([#225](https://github.com/opensearch-project/alerting/pull/225)) + +### Maintenance + +* Update copyright notice ([#222](https://github.com/opensearch-project/alerting/pull/222)) + diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 000000000..7aa6cb535 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# 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. + +set -ex + +function usage() { + echo "Usage: $0 [args]" + echo "" + echo "Arguments:" + echo -e "-v VERSION\t[Required] OpenSearch version." + echo -e "-s SNAPSHOT\t[Optional] Build a snapshot, default is 'false'." + echo -e "-a ARCHITECTURE\t[Optional] Build architecture, ignored." + echo -e "-o OUTPUT\t[Optional] Output path, default is 'artifacts'." + echo -e "-h help" +} + +while getopts ":h:v:s:o:a:" arg; do + case $arg in + h) + usage + exit 1 + ;; + v) + VERSION=$OPTARG + ;; + s) + SNAPSHOT=$OPTARG + ;; + o) + OUTPUT=$OPTARG + ;; + a) + ARCHITECTURE=$OPTARG + ;; + :) + echo "Error: -${OPTARG} requires an argument" + usage + exit 1 + ;; + ?) + echo "Invalid option: -${arg}" + exit 1 + ;; + esac +done + +if [ -z "$VERSION" ]; then + echo "Error: You must specify the OpenSearch version" + usage + exit 1 +fi + +[[ "$SNAPSHOT" == "true" ]] && VERSION=$VERSION-SNAPSHOT +[ -z "$OUTPUT" ] && OUTPUT=artifacts + +mkdir -p $OUTPUT/plugins + +./gradlew assemble --no-daemon --refresh-dependencies -DskipTests=true -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -x ktlint + +zipPath=$(find . -path \*build/distributions/*.zip) +distributions="$(dirname "${zipPath}")" + +echo "COPY ${distributions}/*.zip" +cp ${distributions}/*.zip ./$OUTPUT/plugins + +./gradlew publishShadowPublicationToMavenLocal -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT -x ktlint +./gradlew publishShadowPublicationToStagingRepository -Dopensearch.version=$VERSION -Dbuild.snapshot=$SNAPSHOT + +mkdir -p $OUTPUT/maven/org/opensearch +cp -r ./build/local-staging-repo/org/opensearch/notification $OUTPUT/maven/org/opensearch/notification + +