diff --git a/.github/workflows/security-test-workflow.yml b/.github/workflows/security-test-workflow.yml index 7cb66242c..127962210 100644 --- a/.github/workflows/security-test-workflow.yml +++ b/.github/workflows/security-test-workflow.yml @@ -43,7 +43,12 @@ jobs: 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-$qualifier + if qualifier + then + docker_version=$version-$qualifier + else + docker_version=$version + fi [[ -z $candidate_version ]] && candidate_version=$qualifier && qualifier="" diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexMonitorAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexMonitorAction.kt index c94afa8ae..5aafba1be 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/resthandler/RestIndexMonitorAction.kt @@ -84,6 +84,8 @@ class RestIndexMonitorAction : BaseRestHandler() { val xcp = request.contentParser() ensureExpectedToken(Token.START_OBJECT, xcp.nextToken(), xcp) val monitor = Monitor.parse(xcp, id).copy(lastUpdateTime = Instant.now()) + val rbacRoles = request.contentParser().map()["rbac_roles"] as List? + validateDataSources(monitor) validateOwner(monitor.owner) val monitorType = monitor.monitorType @@ -118,7 +120,7 @@ class RestIndexMonitorAction : BaseRestHandler() { } else { WriteRequest.RefreshPolicy.IMMEDIATE } - val indexMonitorRequest = IndexMonitorRequest(id, seqNo, primaryTerm, refreshPolicy, request.method(), monitor) + val indexMonitorRequest = IndexMonitorRequest(id, seqNo, primaryTerm, refreshPolicy, request.method(), monitor, rbacRoles) return RestChannelConsumer { channel -> client.execute(AlertingActions.INDEX_MONITOR_ACTION_TYPE, indexMonitorRequest, indexMonitorResponse(channel, request.method())) diff --git a/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt b/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt index d5303fc18..4b3a403c7 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/SecureTransportAction.kt @@ -61,7 +61,7 @@ interface SecureTransportAction { /** * 'all_access' role users are treated as admins. */ - private fun isAdmin(user: User?): Boolean { + fun isAdmin(user: User?): Boolean { return when { user == null -> { false 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 a00cde540..0546d0733 100644 --- a/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt +++ b/alerting/src/main/kotlin/org/opensearch/alerting/transport/TransportIndexMonitorAction.kt @@ -123,6 +123,40 @@ class TransportIndexMonitorAction @Inject constructor( return } + if ( + user != null && + !isAdmin(user) && + transformedRequest.rbacRoles != null + ) { + if (transformedRequest.rbacRoles?.stream()?.anyMatch { !user.backendRoles.contains(it) } == true) { + log.debug( + "User specified backend roles, ${transformedRequest.rbacRoles}, " + + "that they don' have access to. User backend roles: ${user.backendRoles}" + ) + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException( + "User specified backend roles that they don't have access to. Contact administrator", RestStatus.FORBIDDEN + ) + ) + ) + return + } else if (transformedRequest.rbacRoles?.isEmpty() == true) { + log.debug( + "Non-admin user are not allowed to specify an empty set of backend roles. " + + "Please don't pass in the parameter or pass in at least one backend role." + ) + actionListener.onFailure( + AlertingException.wrap( + OpenSearchStatusException( + "Non-admin user are not allowed to specify an empty set of backend roles.", RestStatus.FORBIDDEN + ) + ) + ) + return + } + } + if (!isADMonitor(transformedRequest.monitor)) { checkIndicesAndExecute(client, actionListener, transformedRequest, user) } else { @@ -405,6 +439,19 @@ class TransportIndexMonitorAction @Inject constructor( private suspend fun indexMonitor() { var metadata = createMetadata() + if (user != null) { + // Use the backend roles which is an intersection of the requested backend roles and the user's backend roles. + // Admins can pass in any backend role. Also if no backend role is passed in, all the user's backend roles are used. + val rbacRoles = if (request.rbacRoles == null) user.backendRoles.toSet() + else if (!isAdmin(user)) request.rbacRoles?.intersect(user.backendRoles)?.toSet() + else request.rbacRoles + + request.monitor = request.monitor.copy( + user = User(user.name, rbacRoles.orEmpty().toList(), user.roles, user.customAttNames) + ) + log.debug("Created monitor's backend roles: $rbacRoles") + } + val indexRequest = IndexRequest(SCHEDULED_JOBS_INDEX) .setRefreshPolicy(request.refreshPolicy) .source(request.monitor.toXContentWithUser(jsonBuilder(), ToXContent.MapParams(mapOf("with_type" to "true")))) @@ -499,6 +546,42 @@ class TransportIndexMonitorAction @Inject constructor( if (request.monitor.enabled && currentMonitor.enabled) request.monitor = request.monitor.copy(enabledTime = currentMonitor.enabledTime) + /** + * On update monitor check which backend roles to associate to the monitor. + * Below are 2 examples of how the logic works + * + * Example 1, say we have a Monitor with backend roles [a, b, c, d] associated with it. + * If I'm User A (non-admin user) and I have backend roles [a, b, c] associated with me and I make a request to update + * the Monitor's backend roles to [a, b]. This would mean that the roles to remove are [c] and the roles to add are [a, b]. + * The Monitor's backend roles would then be [a, b, d]. + * + * Example 2, say we have a Monitor with backend roles [a, b, c, d] associated with it. + * If I'm User A (admin user) and I have backend roles [a, b, c] associated with me and I make a request to update + * the Monitor's backend roles to [a, b]. This would mean that the roles to remove are [c, d] and the roles to add are [a, b]. + * The Monitor's backend roles would then be [a, b]. + */ + if (user != null) { + if (request.rbacRoles != null) { + if (isAdmin(user)) { + request.monitor = request.monitor.copy( + user = User(user.name, request.rbacRoles, user.roles, user.customAttNames) + ) + } else { + // rolesToRemove: these are the backend roles to remove from the monitor + val rolesToRemove = user.backendRoles - request.rbacRoles.orEmpty() + // remove the monitor's roles with rolesToRemove and add any roles passed into the request.rbacRoles + val updatedRbac = currentMonitor.user?.backendRoles.orEmpty() - rolesToRemove + request.rbacRoles.orEmpty() + request.monitor = request.monitor.copy( + user = User(user.name, updatedRbac, user.roles, user.customAttNames) + ) + } + } else { + request.monitor = request.monitor + .copy(user = User(user.name, currentMonitor.user!!.backendRoles, user.roles, user.customAttNames)) + } + log.debug("Update monitor backend roles to: ${request.monitor.user?.backendRoles}") + } + request.monitor = request.monitor.copy(schemaVersion = IndexUtils.scheduledJobIndexSchemaVersion) val indexRequest = IndexRequest(SCHEDULED_JOBS_INDEX) .setRefreshPolicy(request.refreshPolicy) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AccessRoles.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AccessRoles.kt index 918a1f1c3..d3b73779c 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AccessRoles.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AccessRoles.kt @@ -6,6 +6,7 @@ package org.opensearch.alerting val ALL_ACCESS_ROLE = "all_access" +val READALL_AND_MONITOR_ROLE = "readall_and_monitor" val ALERTING_FULL_ACCESS_ROLE = "alerting_full_access" val ALERTING_READ_ONLY_ACCESS = "alerting_read_access" val ALERTING_NO_ACCESS_ROLE = "no_access" diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt index 647132a1a..bacdd2c40 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/AlertingRestTestCase.kt @@ -106,10 +106,26 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { return entityAsMap(this) } - protected fun createMonitorWithClient(client: RestClient, monitor: Monitor, refresh: Boolean = true): Monitor { + private fun createMonitorEntityWithBackendRoles(monitor: Monitor, rbacRoles: List?): HttpEntity { + if (rbacRoles == null) { + return monitor.toHttpEntity() + } + val temp = monitor.toJsonString() + val toReplace = temp.lastIndexOf("}") + val rbacString = rbacRoles.joinToString { "\"$it\"" } + val jsonString = temp.substring(0, toReplace) + ", \"rbac_roles\": [$rbacString] }" + return StringEntity(jsonString, APPLICATION_JSON) + } + + protected fun createMonitorWithClient( + client: RestClient, + monitor: Monitor, + rbacRoles: List? = null, + refresh: Boolean = true + ): Monitor { val response = client.makeRequest( "POST", "$ALERTING_BASE_URI?refresh=$refresh", emptyMap(), - monitor.toHttpEntity() + createMonitorEntityWithBackendRoles(monitor, rbacRoles) ) assertEquals("Unable to create a new monitor", RestStatus.CREATED, response.restStatus()) @@ -123,7 +139,7 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { } protected fun createMonitor(monitor: Monitor, refresh: Boolean = true): Monitor { - return createMonitorWithClient(client(), monitor, refresh) + return createMonitorWithClient(client(), monitor, emptyList(), refresh) } protected fun deleteMonitor(monitor: Monitor, refresh: Boolean = true): Response { @@ -499,6 +515,21 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { return getMonitor(monitorId = monitor.id) } + protected fun updateMonitorWithClient( + client: RestClient, + monitor: Monitor, + rbacRoles: List = emptyList(), + refresh: Boolean = true + ): Monitor { + val response = client.makeRequest( + "PUT", "${monitor.relativeUrl()}?refresh=$refresh", + emptyMap(), createMonitorEntityWithBackendRoles(monitor, rbacRoles) + ) + assertEquals("Unable to update a monitor", RestStatus.OK, response.restStatus()) + assertUserNull(response.asMap()["monitor"] as Map) + return getMonitor(monitorId = monitor.id) + } + protected fun getMonitor(monitorId: String, header: BasicHeader = BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json")): Monitor { val response = client().makeRequest("GET", "$ALERTING_BASE_URI/$monitorId", null, header) assertEquals("Unable to get monitor $monitorId", RestStatus.OK, response.restStatus()) @@ -1089,6 +1120,18 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { client().performRequest(request) } + fun patchUserBackendRoles(name: String, backendRoles: Array) { + val request = Request("PATCH", "/_plugins/_security/api/internalusers/$name") + val broles = backendRoles.joinToString { "\"$it\"" } + var entity = " [{\n" + + "\"op\": \"replace\",\n" + + "\"path\": \"/backend_roles\",\n" + + "\"value\": [$broles]\n" + + "}]" + request.setJsonEntity(entity) + client().performRequest(request) + } + fun createIndexRole(name: String, index: String) { val request = Request("PUT", "/_plugins/_security/api/roles/$name") var entity = "{\n" + @@ -1174,6 +1217,22 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { client().performRequest(request) } + fun updateRoleMapping(role: String, users: List, addUser: Boolean) { + val request = Request("PATCH", "/_plugins/_security/api/rolesmapping/$role") + val usersStr = users.joinToString { it -> "\"$it\"" } + + val op = if (addUser) "add" else "remove" + + val entity = "[{\n" + + " \"op\" : \"$op\",\n" + + " \"path\" : \"/users\",\n" + + " \"value\" : [$usersStr]\n" + + "}]" + + request.setJsonEntity(entity) + client().performRequest(request) + } + fun deleteUser(name: String) { client().makeRequest("DELETE", "/_plugins/_security/api/internalusers/$name") } @@ -1202,15 +1261,31 @@ abstract class AlertingRestTestCase : ODFERestTestCase() { user: String, index: String, role: String, - backendRole: String, + backendRoles: List, clusterPermissions: String? ) { - createUser(user, user, arrayOf(backendRole)) + createUser(user, user, backendRoles.toTypedArray()) createTestIndex(index) createCustomIndexRole(role, index, clusterPermissions) createUserRolesMapping(role, arrayOf(user)) } + fun createUserWithRoles( + user: String, + roles: List, + backendRoles: List, + isExistingRole: Boolean + ) { + createUser(user, user, backendRoles.toTypedArray()) + for (role in roles) { + if (isExistingRole) { + updateRoleMapping(role, listOf(user), true) + } else { + createUserRolesMapping(role, arrayOf(user)) + } + } + } + fun createUserWithDocLevelSecurityTestData( user: String, index: String, diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureDestinationRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureDestinationRestApiIT.kt index 598b4f98c..35b9cad43 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureDestinationRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureDestinationRestApiIT.kt @@ -139,7 +139,7 @@ class SecureDestinationRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_GET_DESTINATION_ACCESS) ) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailAccountRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailAccountRestApiIT.kt index 74bb75ff7..b27b6285a 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailAccountRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailAccountRestApiIT.kt @@ -76,7 +76,7 @@ class SecureEmailAccountRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_GET_EMAIL_ACCOUNT_ACCESS) ) @@ -105,7 +105,7 @@ class SecureEmailAccountRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_SEARCH_EMAIL_ACCOUNT_ACCESS) ) @@ -132,7 +132,7 @@ class SecureEmailAccountRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_NO_ACCESS_ROLE) ) @@ -162,7 +162,7 @@ class SecureEmailAccountRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_NO_ACCESS_ROLE) ) diff --git a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailGroupsRestApiIT.kt b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailGroupsRestApiIT.kt index 72fb317e1..13e51e62d 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailGroupsRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureEmailGroupsRestApiIT.kt @@ -78,7 +78,7 @@ class SecureEmailGroupsRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_GET_EMAIL_GROUP_ACCESS) ) @@ -105,7 +105,7 @@ class SecureEmailGroupsRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_SEARCH_EMAIL_GROUP_ACCESS) ) 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 9888578d0..4b6f18f50 100644 --- a/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt +++ b/alerting/src/test/kotlin/org/opensearch/alerting/resthandler/SecureMonitorRestApiIT.kt @@ -25,6 +25,7 @@ import org.opensearch.alerting.ALL_ACCESS_ROLE import org.opensearch.alerting.ALWAYS_RUN import org.opensearch.alerting.AlertingRestTestCase import org.opensearch.alerting.DRYRUN_MONITOR +import org.opensearch.alerting.READALL_AND_MONITOR_ROLE import org.opensearch.alerting.TEST_HR_BACKEND_ROLE import org.opensearch.alerting.TEST_HR_INDEX import org.opensearch.alerting.TEST_HR_ROLE @@ -116,7 +117,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_NO_ACCESS_ROLE) ) try { @@ -142,7 +143,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_READ_ONLY_ACCESS) ) try { @@ -169,7 +170,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_SEARCH_MONITOR_ONLY_ACCESS) ) val monitor = createRandomMonitor(true) @@ -196,7 +197,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_NO_ACCESS_ROLE) ) try { @@ -223,7 +224,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_INDEX_MONITOR_ACCESS) ) try { @@ -257,7 +258,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) ) @@ -283,7 +284,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_NO_ACCESS_ROLE) ) @@ -349,6 +350,518 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { assertFalse("The monitor was not disabled", updatedMonitor.enabled) } + fun `test create monitor with enable filter by with a user have access and without role has no access`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf("role2"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, getUser) + .setSocketTimeout(60000).build() + + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + + // Remove backend role and ensure no access is granted after + patchUserBackendRoles(getUser, arrayOf("role1")) + try { + getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + } + } + + fun `test create monitor with enable filter by with no backend roles`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + try { + createMonitorWithClient(userClient!!, monitor = monitor, listOf()) + fail("Expected exception since a non-admin user is trying to create a monitor with no backend roles") + } catch (e: ResponseException) { + assertEquals("Create monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test create monitor as admin with enable filter by with no backend roles`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(client(), monitor = monitor, listOf()) + assertNotNull("The monitor was not created", createdMonitor) + + try { + userClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test create monitor with enable filter by with roles user has no access and throw exception`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + try { + createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role1", "role2")) + fail("Expected create monitor to fail as user does not have role1 backend role") + } catch (e: ResponseException) { + assertEquals("Create monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test create monitor as admin with enable filter by with a user have access and without role has no access`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + val createdMonitor = createMonitorWithClient(client(), monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role1", "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + // user should have access to the admin monitor + createUserWithTestDataAndCustomRole( + user, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf(TEST_HR_BACKEND_ROLE), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + + val getMonitorResponse = userClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + + // Remove good backend role and ensure no access is granted after + patchUserBackendRoles(user, arrayOf("role5")) + try { + userClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + } + } + + fun `test update monitor with enable filter by with removing a permission`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf("role2"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, getUser) + .setSocketTimeout(60000).build() + + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + + // Remove backend role from monitor + val updatedMonitor = updateMonitorWithClient(userClient!!, createdMonitor, listOf(TEST_HR_BACKEND_ROLE)) + + // getUser should no longer have access + try { + getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${updatedMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test update monitor with enable filter by with no backend roles`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf("role2")) + assertNotNull("The monitor was not created", createdMonitor) + + try { + updateMonitorWithClient(userClient!!, createdMonitor, listOf()) + } catch (e: ResponseException) { + assertEquals("Update monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test update monitor as admin with enable filter by with no backend roles`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(client(), monitor = monitor, listOf(TEST_HR_BACKEND_ROLE)) + assertNotNull("The monitor was not created", createdMonitor) + + val getMonitorResponse = userClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + + val updatedMonitor = updateMonitorWithClient(client(), createdMonitor, listOf()) + + try { + userClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${updatedMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test update monitor with enable filter by with updating with a permission user has no access to and throw exception`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf("role2"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, getUser) + .setSocketTimeout(60000).build() + + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + + try { + updateMonitorWithClient(userClient!!, createdMonitor, listOf(TEST_HR_BACKEND_ROLE, "role1")) + fail("Expected update monitor to fail as user doesn't have access to role1") + } catch (e: ResponseException) { + assertEquals("Update monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test update monitor as another user with enable filter by with removing a permission and adding permission`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE)) + assertNotNull("The monitor was not created", createdMonitor) + + // Remove backend role from monitor with new user and add role5 + val updateUser = "updateUser" + createUserWithRoles( + updateUser, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role5"), + false + ) + + val updateUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), updateUser, updateUser) + .setSocketTimeout(60000).build() + val updatedMonitor = updateMonitorWithClient(updateUserClient, createdMonitor, listOf("role5")) + + // old user should no longer have access + try { + userClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${updatedMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteUser(updateUser) + updateUserClient?.close() + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + } + + fun `test update monitor as admin with enable filter by with removing a permission`() { + enableFilterBy() + if (!isHttps()) { + // if security is disabled and filter by is enabled, we can't create monitor + // refer: `test create monitor with enable filter by` + return + } + val monitor = randomQueryLevelMonitor(enabled = true) + + createUserWithRoles( + user, + listOf(ALERTING_FULL_ACCESS_ROLE, READALL_AND_MONITOR_ROLE), + listOf(TEST_HR_BACKEND_ROLE, "role2"), + false + ) + + val createdMonitor = createMonitorWithClient(userClient!!, monitor = monitor, listOf(TEST_HR_BACKEND_ROLE, "role2")) + assertNotNull("The monitor was not created", createdMonitor) + + // getUser should have access to the monitor + val getUser = "getUser" + createUserWithTestDataAndCustomRole( + getUser, + TEST_HR_INDEX, + TEST_HR_ROLE, + listOf("role1", "role2"), + getClusterPermissionsFromCustomRole(ALERTING_GET_MONITOR_ACCESS) + ) + val getUserClient = SecureRestClientBuilder(clusterHosts.toTypedArray(), isHttps(), getUser, getUser) + .setSocketTimeout(60000).build() + + val getMonitorResponse = getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${createdMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + assertEquals("Get monitor failed", RestStatus.OK, getMonitorResponse?.restStatus()) + + // Remove backend role from monitor + val updatedMonitor = updateMonitorWithClient(client(), createdMonitor, listOf("role4")) + + // original user should no longer have access + try { + userClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${updatedMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + createUserRolesMapping(ALERTING_FULL_ACCESS_ROLE, arrayOf()) + createUserRolesMapping(READALL_AND_MONITOR_ROLE, arrayOf()) + } + + // get user should no longer have access + try { + getUserClient?.makeRequest( + "GET", + "$ALERTING_BASE_URI/${updatedMonitor.id}", + null, + BasicHeader(HttpHeaders.CONTENT_TYPE, "application/json") + ) + fail("Expected Forbidden exception") + } catch (e: ResponseException) { + assertEquals("Get monitor failed", RestStatus.FORBIDDEN.status, e.response.statusLine.statusCode) + } finally { + deleteRoleAndRoleMapping(TEST_HR_ROLE) + deleteUser(getUser) + getUserClient?.close() + } + } + fun `test delete monitor with disable filter by`() { disableFilterBy() val monitor = randomQueryLevelMonitor(enabled = true) @@ -513,7 +1026,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_EXECUTE_MONITOR_ACCESS) ) @@ -538,7 +1051,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_NO_ACCESS_ROLE) ) @@ -565,7 +1078,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_DELETE_MONITOR_ACCESS) ) @@ -592,7 +1105,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_NO_ACCESS_ROLE) ) @@ -712,7 +1225,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), getClusterPermissionsFromCustomRole(ALERTING_GET_ALERTS_ACCESS) ) try { @@ -820,7 +1333,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), TERM_DLS_QUERY, getClusterPermissionsFromCustomRole(ALERTING_FULL_ACCESS_ROLE) ) @@ -874,7 +1387,7 @@ class SecureMonitorRestApiIT : AlertingRestTestCase() { user, TEST_HR_INDEX, TEST_HR_ROLE, - TEST_HR_BACKEND_ROLE, + listOf(TEST_HR_BACKEND_ROLE), TERM_DLS_QUERY, getClusterPermissionsFromCustomRole(ALERTING_FULL_ACCESS_ROLE) )