Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Backport 2.x] Add security tests and workflow plus minor fix #497

Merged
merged 1 commit into from
Aug 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/workflows/security-notifications-test-workflow.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
##
# Copyright OpenSearch Contributors
# SPDX-License-Identifier: Apache-2.0
##

name: Security Test and Build Notifications

on: [push, pull_request]

jobs:
build:
strategy:
# This setting says that all jobs should finish, even if one fails
fail-fast: false
matrix:
java: [11, 17]

runs-on: ubuntu-latest

steps:
- name: Set up JDK ${{ matrix.java }}
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}

# notifications
- name: Checkout Notifications
uses: actions/checkout@v2

# Temporarily exclude tests which causing CI to fail. Tracking in #251
- name: Build with Gradle
# Only assembling since the full build is governed by other workflows
run: |
cd notifications
./gradlew assemble

- name: Pull and Run Docker
run: |
plugin_core=`basename $(ls notifications/core/build/distributions/*.zip)`
plugin=`basename $(ls notifications/notifications/build/distributions/*.zip)`
list_of_files=`ls`
list_of_all_files=`ls notifications/core/build/distributions/`
version=`echo $plugin|awk -F- '{print $3}'| cut -d. -f 1-3`
plugin_version=`echo $plugin|awk -F- '{print $3}'| cut -d. -f 1-4`
qualifier=`echo $plugin|awk -F- '{print $4}'| cut -d. -f 1-1`
candidate_version=`echo $plugin|awk -F- '{print $5}'| cut -d. -f 1-1`
docker_version=$version

[[ -z $candidate_version ]] && candidate_version=$qualifier && qualifier=""

echo plugin version plugin_version qualifier candidate_version docker_version
echo "($plugin) ($version) ($plugin_version) ($qualifier) ($candidate_version) ($docker_version)"
echo $ls $list_of_all_files

if docker pull opensearchstaging/opensearch:$docker_version
then
echo "FROM opensearchstaging/opensearch:$docker_version" >> Dockerfile
echo "RUN /usr/share/opensearch/bin/opensearch-plugin remove opensearch-notifications;" >> Dockerfile
echo "RUN /usr/share/opensearch/bin/opensearch-plugin remove opensearch-notifications-core;" >> Dockerfile
echo "ADD notifications/core/build/distributions/$plugin_core /tmp/" >> Dockerfile
echo "RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/$plugin_core" >> Dockerfile
echo "ADD notifications/notifications/build/distributions/$plugin /tmp/" >> Dockerfile
echo "RUN /usr/share/opensearch/bin/opensearch-plugin install --batch file:/tmp/$plugin" >> Dockerfile
docker build -t opensearch-notifications:test .
echo "imagePresent=true" >> $GITHUB_ENV
else
echo "imagePresent=false" >> $GITHUB_ENV
fi

- name: Run Docker Image
if: env.imagePresent == 'true'
run: |
cd ..
docker run -p 9200:9200 -d -p 9600:9600 -e "discovery.type=single-node" opensearch-notifications:test
sleep 120

- name: Run Notification Test for security enabled test cases
if: env.imagePresent == 'true'
run: |
cluster_running=`curl -XGET https://localhost:9200/_cat/plugins -u admin:admin --insecure`
echo $cluster_running
security=`curl -XGET https://localhost:9200/_cat/plugins -u admin:admin --insecure |grep opensearch-security|wc -l`
echo $security
if [ $security -gt 0 ]
then
echo "Security plugin is available"
cd notifications
./gradlew integTest -Dtests.rest.cluster=localhost:9200 -Dtests.cluster=localhost:9200 -Dtests.clustername=docker-cluster -Dhttps=true -Duser=admin -Dpassword=admin
else
echo "Security plugin is NOT available skipping this run as tests without security have already been run"
exit 1
fi
6 changes: 6 additions & 0 deletions notifications/notifications/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ integTest {
jvmArgs '-agentlib:jdwp=transport=dt_socket,server=n,suspend=y,address=8000'
}

if (System.getProperty("https") == null || System.getProperty("https") == "false") {
filter {
excludeTestsMatching "org.opensearch.*.Security*IT"
}
}

if (System.getProperty("tests.rest.bwcsuite") == null) {
filter {
excludeTestsMatching "org.opensearch.integtest.bwc.*IT"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ internal class SendTestNotificationAction @Inject constructor(
::SendTestNotificationRequest
) {
companion object {
private const val NAME = "cluster:admin/opensearch/notifications/test_notification"
internal const val NAME = "cluster:admin/opensearch/notifications/test_notification"
internal val ACTION_TYPE = ActionType(NAME, ::SendNotificationResponse)
private val log by logger(SendTestNotificationAction::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.opensearch.action.search.SearchResponse
import org.opensearch.client.Client
import org.opensearch.cluster.service.ClusterService
import org.opensearch.common.unit.TimeValue
import org.opensearch.common.util.concurrent.ThreadContext
import org.opensearch.common.xcontent.LoggingDeprecationHandler
import org.opensearch.common.xcontent.NamedXContentRegistry
import org.opensearch.common.xcontent.XContentHelper
Expand Down Expand Up @@ -104,18 +105,20 @@ internal object NotificationConfigIndex : ConfigOperations {
val request = CreateIndexRequest(INDEX_NAME)
.mapping(indexMappingAsMap)
.settings(indexSettingsSource, XContentType.YAML)
try {
val response: CreateIndexResponse = client.suspendUntilTimeout(PluginSettings.operationTimeoutMs) {
admin().indices().create(request, it)
}
if (response.isAcknowledged) {
log.info("$LOG_PREFIX:Index $INDEX_NAME creation Acknowledged")
} else {
throw IllegalStateException("$LOG_PREFIX:Index $INDEX_NAME creation not Acknowledged")
}
} catch (exception: Exception) {
if (exception !is ResourceAlreadyExistsException && exception.cause !is ResourceAlreadyExistsException) {
throw exception
client.threadPool().threadContext.stashContext().use {
try {
val response: CreateIndexResponse = client.suspendUntilTimeout(PluginSettings.operationTimeoutMs) {
admin().indices().create(request, it)
}
if (response.isAcknowledged) {
log.info("$LOG_PREFIX:Index $INDEX_NAME creation Acknowledged")
} else {
throw IllegalStateException("$LOG_PREFIX:Index $INDEX_NAME creation not Acknowledged")
}
} catch (exception: Exception) {
if (exception !is ResourceAlreadyExistsException && exception.cause !is ResourceAlreadyExistsException) {
throw exception
}
}
}
}
Expand Down Expand Up @@ -300,3 +303,40 @@ internal object NotificationConfigIndex : ConfigOperations {
return mutableMap
}
}

/**
* Executes the given [block] function on this resource and then closes it down correctly whether an exception
* is thrown or not.
*
* In case if the resource is being closed due to an exception occurred in [block], and the closing also fails with an exception,
* the latter is added to the [suppressed][java.lang.Throwable.addSuppressed] exceptions of the former.
*
* @param block a function to process this [AutoCloseable] resource.
* @return the result of [block] function invoked on this resource.
*/
private inline fun <T : ThreadContext.StoredContext, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e
throw e
} finally {
closeFinally(exception)
}
}

/**
* Closes this [AutoCloseable], suppressing possible exception or error thrown by [AutoCloseable.close] function when
* it's being closed due to some other [cause] exception occurred.
*
* The suppressed exception is added to the list of suppressed exceptions of [cause] exception.
*/
private fun ThreadContext.StoredContext.closeFinally(cause: Throwable?) = when (cause) {
null -> close()
else -> try {
close()
} catch (closeException: Throwable) {
cause.addSuppressed(closeException)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

package org.opensearch.integtest

import org.opensearch.commons.notifications.action.NotificationsActions
import org.opensearch.notifications.action.SendTestNotificationAction

val ALL_ACCESS_ROLE = "all_access"
val NOTIFICATION_FULL_ACCESS_ROLE = "notifications_full_access"
val NOTIFICATION_READ_ONLY_ACCESS = "notifications_read_access"
val NOTIFICATION_NO_ACCESS_ROLE = "no_access"
val NOTIFICATION_CREATE_CONFIG_ACCESS = "notifications_create_config_access"
val NOTIFICATION_UPDATE_CONFIG_ACCESS = "notifications_update_config_access"
val NOTIFICATION_DELETE_CONFIG_ACCESS = "notifications_delete_config_access"
val NOTIFICATION_GET_CONFIG_ACCESS = "notifications_get_config_access"
val NOTIFICATION_GET_PLUGIN_FEATURE_ACCESS = "notifications_get_plugin_access"
val NOTIFICATION_GET_CHANNEL_ACCESS = "notifications_get_channel_access"
val NOTIFICATION_TEST_SEND_ACCESS = "notifications_test_send_access"

val ROLE_TO_PERMISSION_MAPPING = mapOf(
NOTIFICATION_NO_ACCESS_ROLE to "",
NOTIFICATION_CREATE_CONFIG_ACCESS to NotificationsActions.CREATE_NOTIFICATION_CONFIG_NAME,
NOTIFICATION_UPDATE_CONFIG_ACCESS to NotificationsActions.UPDATE_NOTIFICATION_CONFIG_NAME,
NOTIFICATION_DELETE_CONFIG_ACCESS to NotificationsActions.DELETE_NOTIFICATION_CONFIG_NAME,
NOTIFICATION_GET_CONFIG_ACCESS to NotificationsActions.GET_NOTIFICATION_CONFIG_NAME,
NOTIFICATION_GET_PLUGIN_FEATURE_ACCESS to NotificationsActions.GET_PLUGIN_FEATURES_NAME,
NOTIFICATION_GET_CHANNEL_ACCESS to NotificationsActions.GET_CHANNEL_LIST_NAME,
NOTIFICATION_TEST_SEND_ACCESS to SendTestNotificationAction.NAME
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser
import org.junit.Assert
import org.opensearch.client.Response
import org.opensearch.commons.notifications.model.ConfigType
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.nio.charset.StandardCharsets
import java.time.Instant
import kotlin.random.Random
import kotlin.test.assertEquals
import kotlin.test.assertTrue

Expand Down Expand Up @@ -99,6 +101,68 @@ fun getStatusText(response: JsonObject): String {
.get("status_text").asString
}

private val charPool: List<Char> = ('a'..'z') + ('A'..'Z') + ('0'..'9')

fun getCreateNotificationRequestJsonString(
nameSubstring: String,
configType: ConfigType,
isEnabled: Boolean,
smtpAccountId: String = "",
emailGroupId: Set<String> = setOf()
): String {
val randomString = (1..20)
.map { Random.nextInt(0, charPool.size) }
.map(charPool::get)
.joinToString("")
val configObjectString = when (configType) {
ConfigType.SLACK -> """
"slack":{"url":"https://slack.domain.com/sample_slack_url#$randomString"}
""".trimIndent()
ConfigType.CHIME -> """
"chime":{"url":"https://chime.domain.com/sample_chime_url#$randomString"}
""".trimIndent()
ConfigType.WEBHOOK -> """
"webhook":{"url":"https://web.domain.com/sample_web_url#$randomString"}
""".trimIndent()
ConfigType.SMTP_ACCOUNT -> """
"smtp_account":{
"host":"smtp.domain.com",
"port":"4321",
"method":"ssl",
"from_address":"$randomString@from.com"
}
""".trimIndent()
ConfigType.EMAIL_GROUP -> """
"email_group":{
"recipient_list":[
{"recipient":"$randomString+recipient1@from.com"},
{"recipient":"$randomString+recipient2@from.com"}
]
}
""".trimIndent()
ConfigType.EMAIL -> """
"email":{
"email_account_id":"$smtpAccountId",
"recipient_list":[{"recipient":"$randomString@from.com"}],
"email_group_id_list":[${emailGroupId.joinToString { "\"$it\"" }}]
}
""".trimIndent()
else -> throw IllegalArgumentException("Unsupported configType=$configType")
}
return """
{
"config_id":"$randomString",
"config":{
"name":"$nameSubstring:this is a sample config name $randomString",
"description":"this is a sample config description $randomString",
"config_type":"$configType",
"is_enabled":$isEnabled,
$configObjectString
}
}
""".trimIndent()
}

/** Util class to build Json entity of request body */
class NotificationsJsonEntity(
private val refTag: String?,
Expand Down
Loading