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

Release 1.2 #226

Merged
merged 2 commits into from
Nov 8, 2021
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
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