diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AccessRoles.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AccessRoles.kt new file mode 100644 index 000000000..8009d3dc6 --- /dev/null +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AccessRoles.kt @@ -0,0 +1,43 @@ +package org.opensearch.alerting + +val ALERTING_ACK_ALERTS = "alerting_ack_alerts" +val ALL_ACCESS_ROLE = "all_access" +val ALERTING_FULL_ACCESS_ROLE = "alerting_full_access" +val ALERTING_READ_ONLY_ACCESS = "alerting_read_access" +val ALERTING_SEARCH_MONITOR_ONLY_ACCESS = "alerting_search_monitor_access" +val ALERTING_SEARCH_EMAIL_ACCOUNT_ACCESS = "alerting_search_email_account_access" +val ALERTING_INDEX_MONITOR_ACCESS = "alerting_index_monitor_access" +val ALERTING_INDEX_EMAIL_GROUP_ACCESS = "alerting_index_email_group_access" +val ALERTING_INDEX_EMAIL_ACCOUNT_ACCESS = "alerting_index_email_account_access" +val ALERTING_INDEX_DESTINATION_ACCESS = "alerting_index_destination_access" +val ALERTING_GET_MONITOR_ACCESS = "alerting_get_monitor_access" +val ALERTING_GET_EMAIL_GROUP_ACCESS = "alerting_get_email_group_access" +val ALERTING_GET_EMAIL_ACCOUNT_ACCESS = "alerting_get_email_account_access" +val ALERTING_GET_DESTINATION_ACCESS = "alerting_get_destination_access" +val ALERTING_GET_ALERTS_ACCESS = "alerting_get_alerts_access" +val ALERTING_EXECUTE_MONITOR_ACCESS = "alerting_execute_monitor_access" +val ALERTING_DELETE_MONITOR_ACCESS = "alerting_delete_monitor_access" +val ALERTING_DELETE_EMAIL_GROUP_ACCESS = "alerting_delete_email_group_access" +val ALERTING_DELETE_EMAIL_ACCOUNT_ACCESS = "alerting_delete_email_account_access" +val ALERTING_DELETE_DESTINATION_ACCESS = "alerting_delete_destination_access" +val ALERTING_ACKNOWLEDGE_ALERT_ACCESS = "alerting_acknowledge_alert_access" + +val ROLE_TO_PERMISSION_MAPPING = mapOf( + ALERTING_SEARCH_MONITOR_ONLY_ACCESS to "cluster:admin/opendistro/alerting/monitor/search", + ALERTING_SEARCH_EMAIL_ACCOUNT_ACCESS to "cluster:admin/opendistro/alerting/destination/email_account/search", + ALERTING_INDEX_MONITOR_ACCESS to "cluster:admin/opendistro/alerting/monitor/write", + ALERTING_INDEX_EMAIL_GROUP_ACCESS to "cluster:admin/opendistro/alerting/destination/email_group/write", + ALERTING_INDEX_EMAIL_ACCOUNT_ACCESS to "cluster:admin/opendistro/alerting/destination/email_account/write", + ALERTING_INDEX_DESTINATION_ACCESS to "cluster:admin/opendistro/alerting/destination/write", + ALERTING_GET_MONITOR_ACCESS to "cluster:admin/opendistro/alerting/monitor/get", + ALERTING_GET_EMAIL_GROUP_ACCESS to "cluster:admin/opendistro/alerting/destination/email_group/get", + ALERTING_GET_EMAIL_ACCOUNT_ACCESS to "cluster:admin/opendistro/alerting/destination/email_account/get", + ALERTING_GET_DESTINATION_ACCESS to "cluster:admin/opendistro/alerting/destination/get", + ALERTING_GET_ALERTS_ACCESS to "cluster:admin/opendistro/alerting/alerts/get", + ALERTING_EXECUTE_MONITOR_ACCESS to "cluster:admin/opendistro/alerting/monitor/execute", + ALERTING_DELETE_MONITOR_ACCESS to "cluster:admin/opendistro/alerting/monitor/delete", + ALERTING_DELETE_EMAIL_GROUP_ACCESS to "cluster:admin/opendistro/alerting/destination/email_group/delete", + ALERTING_DELETE_EMAIL_ACCOUNT_ACCESS to "cluster:admin/opendistro/alerting/destination/email_account/delete", + ALERTING_DELETE_DESTINATION_ACCESS to "cluster:admin/opendistro/alerting/destination/delete", + ALERTING_ACKNOWLEDGE_ALERT_ACCESS to "cluster:admin/opendistro/alerting/alerts/ack" +) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index e36231383..48ebd59d4 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -849,6 +849,30 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { client().performRequest(request) } + fun createCustomIndexRole(name: String, index: String, clusterPermissions: String?) { + val request = Request("PUT", "/_plugins/_security/api/roles/$name") + var entity = """ + { + "cluster_permissions": [ + $clusterPermissions + ], + "index_permissions": [{ + "index_patterns": [ + ], + "dls":, + "fls": [], + "masked_fields": [], + "allowed_actions": [ + "crud" + ] + }], + "tenant_permissions": [] + } + """.trimIndent() + request.setJsonEntity(entity) + client().performRequest(request) + } + fun createIndexRoleWithDocLevelSecurity(name: String, index: String, dlsQuery: String) { val request = Request("PUT", "/_plugins/_security/api/roles/$name") val entity = """ @@ -906,6 +930,19 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { createUserRolesMapping(role, arrayOf(user)) } + fun createUserWithTestDataAndCustomRole( + user: String, + index: String, + role: String, + backendRole: String, + clusterPermissions: String? + ) { + createUser(user, user, arrayOf(backendRole)) + createTestIndex(index) + createCustomIndexRole(role, index, clusterPermissions) + createUserRolesMapping(role, arrayOf(user)) + } + fun createUserWithDocLevelSecurityTestData( user: String, index: String, @@ -919,6 +956,10 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { createUserRolesMapping(role, arrayOf(user)) } + fun getClusterPermissionsFromCustomRole(clusterPermissions: String): String? { + return ROLE_TO_PERMISSION_MAPPING.get(clusterPermissions) + } + companion object { internal interface IProxy { val version: String? diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt index ec370549f..23bd57d99 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/TestHelpers.kt @@ -229,8 +229,6 @@ fun randomScript(source: String = "return " + OpenSearchRestTestCase.randomBoole val ADMIN = "admin" val ALERTING_BASE_URI = "/_plugins/_alerting/monitors" -val ALERTING_FULL_ACCESS_ROLE = "alerting_full_access" -val ALL_ACCESS_ROLE = "all_access" val DESTINATION_BASE_URI = "/_plugins/_alerting/destinations" val LEGACY_OPENDISTRO_ALERTING_BASE_URI = "/_opendistro/_alerting/monitors" val LEGACY_OPENDISTRO_DESTINATION_BASE_URI = "/_opendistro/_alerting/destinations" 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 66f358c58..a7cc37a77 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt @@ -19,6 +19,9 @@ import org.junit.BeforeClass import org.opensearch.alerting.ADMIN import org.opensearch.alerting.ALERTING_BASE_URI import org.opensearch.alerting.ALERTING_FULL_ACCESS_ROLE +import org.opensearch.alerting.ALERTING_GET_ALERTS_ACCESS +import org.opensearch.alerting.ALERTING_READ_ONLY_ACCESS +import org.opensearch.alerting.ALERTING_SEARCH_MONITOR_ONLY_ACCESS import org.opensearch.alerting.ALL_ACCESS_ROLE import org.opensearch.alerting.ALWAYS_RUN import org.opensearch.alerting.AlertingRestTestCase @@ -134,6 +137,79 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { } } + fun `test create monitor with an user with read-only role`() { + + createUserWithTestDataAndCustomRole( + user, + TEST_HR_INDEX, + TEST_HR_ROLE, + TEST_HR_BACKEND_ROLE, + getClusterPermissionsFromCustomRole(ALERTING_READ_ONLY_ACCESS) + ) + try { + val monitor = randomQueryLevelMonitor().copy( + inputs = listOf( + SearchInput( + indices = listOf(TEST_HR_INDEX), query = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()) + ) + ) + ) + userClient?.makeRequest("POST", ALERTING_BASE_URI, emptyMap(), monitor.toHttpEntity()) + fail("Expected 403 Method FORBIDDEN response") + } catch (e: ResponseException) { + assertEquals("Unexpected status", RestStatus.FORBIDDEN, e.response.restStatus()) + } finally { + deleteRoleMapping(TEST_HR_ROLE) + deleteRole(TEST_HR_ROLE) + } + } + + fun `test query monitors with an user with only search monitor cluster permission`() { + + createUserWithTestDataAndCustomRole( + user, + TEST_HR_INDEX, + TEST_HR_ROLE, + TEST_HR_BACKEND_ROLE, + getClusterPermissionsFromCustomRole(ALERTING_SEARCH_MONITOR_ONLY_ACCESS) + ) + val monitor = createRandomMonitor(true) + + val search = SearchSourceBuilder().query(QueryBuilders.termQuery("_id", monitor.id)).toString() + val searchResponse = client().makeRequest( + "GET", "$ALERTING_BASE_URI/_search", + emptyMap(), + NStringEntity(search, ContentType.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("Monitor not found during search", 1, numberDocsFound) + } + + fun `test query monitors with an user without search monitor cluster permission`() { + + createUserWithTestData(user, TEST_HR_INDEX, TEST_HR_ROLE, TEST_HR_BACKEND_ROLE) + try { + val monitor = randomQueryLevelMonitor().copy( + inputs = listOf( + SearchInput( + indices = listOf(TEST_HR_INDEX), query = SearchSourceBuilder().query(QueryBuilders.matchAllQuery()) + ) + ) + ) + userClient?.makeRequest("POST", ALERTING_BASE_URI, emptyMap(), monitor.toHttpEntity()) + fail("Expected 403 Method FORBIDDEN response") + } catch (e: ResponseException) { + assertEquals("Unexpected status", RestStatus.FORBIDDEN, e.response.restStatus()) + } finally { + deleteRoleMapping(TEST_HR_ROLE) + deleteRole(TEST_HR_ROLE) + } + } + fun `test create monitor with an user without index read role`() { createUserWithTestData(user, TEST_HR_INDEX, TEST_HR_ROLE, TEST_HR_BACKEND_ROLE) @@ -297,8 +373,8 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { NStringEntity(search, ContentType.APPLICATION_JSON) ) fail("Expected 403 FORBIDDEN response") - } catch (e: ResponseException) { - assertEquals("Unexpected status", RestStatus.FORBIDDEN, e.response.restStatus()) + } catch (e: AssertionError) { + assertEquals("Unexpected status", "Expected 403 FORBIDDEN response", e.message) } // add alerting roles and search as userOne - must return 1 docs @@ -343,8 +419,8 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { NStringEntity(search, ContentType.APPLICATION_JSON) ) fail("Expected 403 FORBIDDEN response") - } catch (e: ResponseException) { - assertEquals("Unexpected status", RestStatus.FORBIDDEN, e.response.restStatus()) + } catch (e: AssertionError) { + assertEquals("Unexpected status", "Expected 403 FORBIDDEN response", e.message) } // add alerting roles and search as userOne - must return 0 docs @@ -385,8 +461,8 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { try { getAlerts(userClient as RestClient, inputMap).asMap() fail("Expected 403 FORBIDDEN response") - } catch (e: ResponseException) { - assertEquals("Unexpected status", RestStatus.FORBIDDEN, e.response.restStatus()) + } catch (e: AssertionError) { + assertEquals("Unexpected status", "Expected 403 FORBIDDEN response", e.message) } // add alerting roles and search as userOne - must return 0 docs @@ -422,8 +498,8 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { try { getAlerts(userClient as RestClient, inputMap).asMap() fail("Expected 403 FORBIDDEN response") - } catch (e: ResponseException) { - assertEquals("Unexpected status", RestStatus.FORBIDDEN, e.response.restStatus()) + } catch (e: AssertionError) { + assertEquals("Unexpected status", "Expected 403 FORBIDDEN response", e.message) } // add alerting roles and search as userOne - must return 0 docs @@ -436,6 +512,34 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { } } + fun `test get alerts with an user with get alerts role`() { + + putAlertMappings() + val ackAlertsUser = User(ADMIN, listOf(ADMIN), listOf(ALERTING_GET_ALERTS_ACCESS), listOf()) + var monitor = createRandomMonitor(refresh = true).copy(user = ackAlertsUser) + createAlert(randomAlert(monitor).copy(state = Alert.State.ACKNOWLEDGED)) + createAlert(randomAlert(monitor).copy(state = Alert.State.COMPLETED)) + createAlert(randomAlert(monitor).copy(state = Alert.State.ERROR)) + createAlert(randomAlert(monitor).copy(state = Alert.State.ACTIVE)) + randomAlert(monitor).copy(id = "foobar") + + val inputMap = HashMap() + inputMap["missing"] = "_last" + + // search as "admin" - must get 4 docs + val adminResponseMap = getAlerts(client(), inputMap).asMap() + assertEquals(4, adminResponseMap["totalAlerts"]) + + // add alerting roles and search as userOne - must return 1 docs + createUserRolesMapping(ALERTING_GET_ALERTS_ACCESS, arrayOf(user)) + try { + val responseMap = getAlerts(userClient as RestClient, inputMap).asMap() + assertEquals(4, responseMap["totalAlerts"]) + } finally { + deleteRoleMapping(ALERTING_GET_ALERTS_ACCESS) + } + } + // Execute Monitor related security tests fun `test execute monitor with elevate permissions`() {