Skip to content

Commit

Permalink
Cherry-pick commits to 1.x (#227)
Browse files Browse the repository at this point in the history
* Update copyright notice (#222)

Signed-off-by: Mohammad Qureshi <qreshi@amazon.com>

* Admin Users must be able to access all monitors #139 (#220)

* Admin Users must be able to access all monitors #139

* Refactored

Co-authored-by: Mohammad Qureshi <47198598+qreshi@users.noreply.github.com>
Co-authored-by: Sriram <59816283+skkosuri-amzn@users.noreply.github.com>
  • Loading branch information
3 people authored Nov 6, 2021
1 parent 60b2548 commit 6836f03
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 168 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copyright OpenSearch Contributors. See [NOTICE](NOTICE.txt) for details.
Original file line number Diff line number Diff line change
@@ -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<String>(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 <T : Any> validateUserBackendRoles(user: User?, actionListener: ActionListener<T>): 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 <T : Any> checkUserPermissionsWithResource(
requesterUser: User?,
resourceUser: User?,
actionListener: ActionListener<T>,
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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -71,22 +68,21 @@ class TransportDeleteDestinationAction @Inject constructor(
val xContentRegistry: NamedXContentRegistry
) : HandledTransportAction<DeleteDestinationRequest, DeleteResponse>(
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<DeleteResponse>) {
val userStr = client.threadPool().threadContext.getTransient<String>(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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -69,22 +66,21 @@ class TransportDeleteMonitorAction @Inject constructor(
val xContentRegistry: NamedXContentRegistry
) : HandledTransportAction<DeleteMonitorRequest, DeleteResponse>(
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<DeleteResponse>) {
val userStr = client.threadPool().threadContext.getTransient<String>(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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -72,24 +71,21 @@ class TransportGetAlertsAction @Inject constructor(
val xContentRegistry: NamedXContentRegistry
) : HandledTransportAction<GetAlertsRequest, GetAlertsResponse>(
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(
task: Task,
getAlertsRequest: GetAlertsRequest,
actionListener: ActionListener<GetAlertsResponse>
) {
val userStr = client.threadPool().threadContext.getTransient<String>(
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
Expand Down Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -75,25 +74,21 @@ class TransportGetDestinationsAction @Inject constructor(
val xContentRegistry: NamedXContentRegistry
) : HandledTransportAction<GetDestinationsRequest, GetDestinationsResponse> (
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(
task: Task,
getDestinationsRequest: GetDestinationsRequest,
actionListener: ActionListener<GetDestinationsResponse>
) {
val userStr = client.threadPool().threadContext.getTransient<String>(
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
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 6836f03

Please sign in to comment.