diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobTransportAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobTransportAction.java index 1fb661d9..7787def6 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobTransportAction.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobTransportAction.java @@ -15,9 +15,10 @@ package com.amazon.opendistroforelasticsearch.ad.transport; +import static com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES; import static com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings.REQUEST_TIMEOUT; - -import java.io.IOException; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.getUserContext; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.resolveUserAndExecute; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -25,6 +26,7 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; @@ -35,61 +37,86 @@ import com.amazon.opendistroforelasticsearch.ad.indices.AnomalyDetectionIndices; import com.amazon.opendistroforelasticsearch.ad.rest.handler.IndexAnomalyDetectorJobActionHandler; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; import com.amazon.opendistroforelasticsearch.ad.util.RestHandlerUtils; +import com.amazon.opendistroforelasticsearch.commons.authuser.User; public class AnomalyDetectorJobTransportAction extends HandledTransportAction { private final Logger logger = LogManager.getLogger(AnomalyDetectorJobTransportAction.class); private final Client client; + private final ClusterService clusterService; private final Settings settings; private final AnomalyDetectionIndices anomalyDetectionIndices; private final NamedXContentRegistry xContentRegistry; + private volatile Boolean filterByEnabled; @Inject public AnomalyDetectorJobTransportAction( TransportService transportService, ActionFilters actionFilters, Client client, + ClusterService clusterService, Settings settings, AnomalyDetectionIndices anomalyDetectionIndices, NamedXContentRegistry xContentRegistry ) { super(AnomalyDetectorJobAction.NAME, transportService, actionFilters, AnomalyDetectorJobRequest::new); this.client = client; + this.clusterService = clusterService; this.settings = settings; this.anomalyDetectionIndices = anomalyDetectionIndices; this.xContentRegistry = xContentRegistry; + filterByEnabled = AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); } @Override protected void doExecute(Task task, AnomalyDetectorJobRequest request, ActionListener listener) { String detectorId = request.getDetectorID(); - long seqNo = request.getSeqNo(); - long primaryTerm = request.getPrimaryTerm(); - String rawPath = request.getRawPath(); - TimeValue requestTimeout = REQUEST_TIMEOUT.get(settings); - // By the time request reaches here, the user permissions are validated by Security plugin. - // Since the detectorID is provided, this can only happen if User is part of a role which has access - // to the detector. This is filtered by our Search Detector API. - + User user = getUserContext(client); try (ThreadContext.StoredContext context = client.threadPool().getThreadContext().stashContext()) { - IndexAnomalyDetectorJobActionHandler handler = new IndexAnomalyDetectorJobActionHandler( - client, - listener, - anomalyDetectionIndices, + resolveUserAndExecute( + user, detectorId, - seqNo, - primaryTerm, - requestTimeout, + filterByEnabled, + listener, + () -> adJobExecute(request, listener), + client, + clusterService, xContentRegistry ); + } catch (Exception e) { + logger.error(e); + listener.onFailure(e); + } + } + + private void adJobExecute(AnomalyDetectorJobRequest request, ActionListener listener) { + String detectorId = request.getDetectorID(); + long seqNo = request.getSeqNo(); + long primaryTerm = request.getPrimaryTerm(); + String rawPath = request.getRawPath(); + TimeValue requestTimeout = REQUEST_TIMEOUT.get(settings); + + IndexAnomalyDetectorJobActionHandler handler = new IndexAnomalyDetectorJobActionHandler( + client, + listener, + anomalyDetectionIndices, + detectorId, + seqNo, + primaryTerm, + requestTimeout, + xContentRegistry + ); + try { if (rawPath.endsWith(RestHandlerUtils.START_JOB)) { handler.startAnomalyDetectorJob(); } else if (rawPath.endsWith(RestHandlerUtils.STOP_JOB)) { handler.stopAnomalyDetectorJob(detectorId); } - } catch (IOException e) { + } catch (Exception e) { logger.error(e); listener.onFailure(e); } diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorTransportAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorTransportAction.java index b7a84109..eec34603 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorTransportAction.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorTransportAction.java @@ -16,6 +16,9 @@ package com.amazon.opendistroforelasticsearch.ad.transport; import static com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; +import static com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.getUserContext; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.resolveUserAndExecute; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import java.io.IOException; @@ -35,6 +38,7 @@ import org.elasticsearch.client.Client; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; @@ -47,7 +51,9 @@ import com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetectorJob; import com.amazon.opendistroforelasticsearch.ad.model.DetectorInternalState; import com.amazon.opendistroforelasticsearch.ad.rest.handler.AnomalyDetectorFunction; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; import com.amazon.opendistroforelasticsearch.ad.util.RestHandlerUtils; +import com.amazon.opendistroforelasticsearch.commons.authuser.User; public class DeleteAnomalyDetectorTransportAction extends HandledTransportAction { @@ -55,6 +61,7 @@ public class DeleteAnomalyDetectorTransportAction extends HandledTransportAction private final Client client; private final ClusterService clusterService; private NamedXContentRegistry xContentRegistry; + private volatile Boolean filterByEnabled; @Inject public DeleteAnomalyDetectorTransportAction( @@ -62,24 +69,34 @@ public DeleteAnomalyDetectorTransportAction( ActionFilters actionFilters, Client client, ClusterService clusterService, + Settings settings, NamedXContentRegistry xContentRegistry ) { super(DeleteAnomalyDetectorAction.NAME, transportService, actionFilters, DeleteAnomalyDetectorRequest::new); this.client = client; this.clusterService = clusterService; this.xContentRegistry = xContentRegistry; + filterByEnabled = AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); } @Override protected void doExecute(Task task, DeleteAnomalyDetectorRequest request, ActionListener listener) { String detectorId = request.getDetectorID(); LOG.info("Delete anomaly detector job {}", detectorId); - + User user = getUserContext(client); // By the time request reaches here, the user permissions are validated by Security plugin. - // Since the detectorID is provided, this can only happen if User is part of a role which has access - // to the detector. This is filtered by our Search Detector API. try (ThreadContext.StoredContext context = client.threadPool().getThreadContext().stashContext()) { - getDetectorJob(detectorId, listener, () -> deleteAnomalyDetectorJobDoc(detectorId, listener)); + resolveUserAndExecute( + user, + detectorId, + filterByEnabled, + listener, + () -> getDetectorJob(detectorId, listener, () -> deleteAnomalyDetectorJobDoc(detectorId, listener)), + client, + clusterService, + xContentRegistry + ); } catch (Exception e) { LOG.error(e); listener.onFailure(e); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportAction.java index 056d1a4d..d3aa09fb 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportAction.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportAction.java @@ -17,6 +17,9 @@ import static com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetector.ANOMALY_DETECTORS_INDEX; import static com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetectorJob.ANOMALY_DETECTOR_JOB_INDEX; +import static com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.getUserContext; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.resolveUserAndExecute; import static com.amazon.opendistroforelasticsearch.ad.util.RestHandlerUtils.PROFILE; import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; @@ -37,9 +40,11 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.CheckedConsumer; import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; @@ -58,12 +63,14 @@ import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; import com.amazon.opendistroforelasticsearch.ad.util.DiscoveryNodeFilterer; import com.amazon.opendistroforelasticsearch.ad.util.RestHandlerUtils; +import com.amazon.opendistroforelasticsearch.commons.authuser.User; import com.google.common.collect.Sets; public class GetAnomalyDetectorTransportAction extends HandledTransportAction { private static final Logger LOG = LogManager.getLogger(GetAnomalyDetectorTransportAction.class); + private final ClusterService clusterService; private final Client client; private final Set allProfileTypeStrs; @@ -74,16 +81,20 @@ public class GetAnomalyDetectorTransportAction extends HandledTransportAction defaultEntityProfileTypes; private final NamedXContentRegistry xContentRegistry; private final DiscoveryNodeFilterer nodeFilter; + private volatile Boolean filterByEnabled; @Inject public GetAnomalyDetectorTransportAction( TransportService transportService, DiscoveryNodeFilterer nodeFilter, ActionFilters actionFilters, + ClusterService clusterService, Client client, + Settings settings, NamedXContentRegistry xContentRegistry ) { super(GetAnomalyDetectorAction.NAME, transportService, actionFilters, GetAnomalyDetectorRequest::new); + this.clusterService = clusterService; this.client = client; List allProfiles = Arrays.asList(DetectorProfileName.values()); @@ -100,10 +111,32 @@ public GetAnomalyDetectorTransportAction( this.xContentRegistry = xContentRegistry; this.nodeFilter = nodeFilter; + filterByEnabled = AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); } @Override protected void doExecute(Task task, GetAnomalyDetectorRequest request, ActionListener listener) { + String detectorID = request.getDetectorID(); + User user = getUserContext(client); + try (ThreadContext.StoredContext context = client.threadPool().getThreadContext().stashContext()) { + resolveUserAndExecute( + user, + detectorID, + filterByEnabled, + listener, + () -> getExecute(request, listener), + client, + clusterService, + xContentRegistry + ); + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } + + protected void getExecute(GetAnomalyDetectorRequest request, ActionListener listener) { String detectorID = request.getDetectorID(); Long version = request.getVersion(); String typesStr = request.getTypeStr(); @@ -112,7 +145,7 @@ protected void doExecute(Task task, GetAnomalyDetectorRequest request, ActionLis boolean all = request.isAll(); boolean returnJob = request.isReturnJob(); - try (ThreadContext.StoredContext context = client.threadPool().getThreadContext().stashContext()) { + try { if (!Strings.isEmpty(typesStr) || rawPath.endsWith(PROFILE) || rawPath.endsWith(PROFILE + "/")) { if (entityValue != null) { Set entityProfilesToCollect = getEntityProfilesToCollect(typesStr, all); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportAction.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportAction.java index c44ea317..64e9aac0 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportAction.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportAction.java @@ -15,6 +15,9 @@ package com.amazon.opendistroforelasticsearch.ad.transport; +import static com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.checkFilterByBackendRoles; +import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.getDetector; import static com.amazon.opendistroforelasticsearch.ad.util.ParseUtils.getUserContext; import java.io.IOException; @@ -44,6 +47,7 @@ import com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetector; import com.amazon.opendistroforelasticsearch.ad.rest.handler.AnomalyDetectorFunction; import com.amazon.opendistroforelasticsearch.ad.rest.handler.IndexAnomalyDetectorActionHandler; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; import com.amazon.opendistroforelasticsearch.commons.authuser.User; public class IndexAnomalyDetectorTransportAction extends HandledTransportAction { @@ -52,6 +56,7 @@ public class IndexAnomalyDetectorTransportAction extends HandledTransportAction< private final AnomalyDetectionIndices anomalyDetectionIndices; private final ClusterService clusterService; private final NamedXContentRegistry xContentRegistry; + private volatile Boolean filterByEnabled; @Inject public IndexAnomalyDetectorTransportAction( @@ -68,11 +73,59 @@ public IndexAnomalyDetectorTransportAction( this.clusterService = clusterService; this.anomalyDetectionIndices = anomalyDetectionIndices; this.xContentRegistry = xContentRegistry; + filterByEnabled = AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.get(settings); + clusterService.getClusterSettings().addSettingsUpdateConsumer(FILTER_BY_BACKEND_ROLES, it -> filterByEnabled = it); } @Override protected void doExecute(Task task, IndexAnomalyDetectorRequest request, ActionListener listener) { User user = getUserContext(client); + String detectorId = request.getDetectorID(); + RestRequest.Method method = request.getMethod(); + try (ThreadContext.StoredContext context = client.threadPool().getThreadContext().stashContext()) { + resolveUserAndExecute(user, detectorId, method, listener, () -> adExecute(request, user, listener)); + } catch (Exception e) { + LOG.error(e); + listener.onFailure(e); + } + } + + private void resolveUserAndExecute( + User requestedUser, + String detectorId, + RestRequest.Method method, + ActionListener listener, + AnomalyDetectorFunction function + ) { + if (requestedUser == null) { + // Security is disabled or user is superadmin + function.execute(); + } else if (!filterByEnabled) { + // security is enabled and filterby is disabled. + function.execute(); + } else { + // security is enabled and filterby is enabled. + try { + // Check if user has backend roles + // When filter by is enabled, block users creating/updating detectors who do not have backend roles. + if (!checkFilterByBackendRoles(requestedUser, listener)) { + return; + } + if (method == RestRequest.Method.PUT) { + // Update detector request, check if user has permissions to update the detector + // Get detector and verify backend roles + getDetector(requestedUser, detectorId, listener, function, client, clusterService, xContentRegistry); + } else { + // Create Detector + function.execute(); + } + } catch (Exception e) { + listener.onFailure(e); + } + } + } + + protected void adExecute(IndexAnomalyDetectorRequest request, User user, ActionListener listener) { anomalyDetectionIndices.updateMappingIfNecessary(); String detectorId = request.getDetectorID(); long seqNo = request.getSeqNo(); @@ -86,7 +139,7 @@ protected void doExecute(Task task, IndexAnomalyDetectorRequest request, ActionL Integer maxAnomalyFeatures = request.getMaxAnomalyFeatures(); checkIndicesAndExecute(detector.getIndices(), () -> { - try (ThreadContext.StoredContext context = client.threadPool().getThreadContext().stashContext()) { + try { IndexAnomalyDetectorActionHandler indexAnomalyDetectorActionHandler = new IndexAnomalyDetectorActionHandler( clusterService, client, @@ -127,14 +180,7 @@ private void checkIndicesAndExecute( SearchRequest searchRequest = new SearchRequest() .indices(indices.toArray(new String[0])) .source(new SearchSourceBuilder().size(1).query(QueryBuilders.matchAllQuery())); - client.search(searchRequest, ActionListener.wrap(r -> { - try (ThreadContext.StoredContext context = client.threadPool().getThreadContext().stashContext()) { - function.execute(); - } catch (Exception e) { - LOG.error(e); - listener.onFailure(e); - } - }, e -> { + client.search(searchRequest, ActionListener.wrap(r -> { function.execute(); }, e -> { // Due to below issue with security plugin, we get security_exception when invalid index name is mentioned. // https://github.com/opendistro-for-elasticsearch/security/issues/718 LOG.error(e); diff --git a/src/main/java/com/amazon/opendistroforelasticsearch/ad/util/ParseUtils.java b/src/main/java/com/amazon/opendistroforelasticsearch/ad/util/ParseUtils.java index e3c4d584..68e515de 100644 --- a/src/main/java/com/amazon/opendistroforelasticsearch/ad/util/ParseUtils.java +++ b/src/main/java/com/amazon/opendistroforelasticsearch/ad/util/ParseUtils.java @@ -17,6 +17,7 @@ import static com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetector.QUERY_PARAM_PERIOD_END; import static com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetector.QUERY_PARAM_PERIOD_START; +import static org.elasticsearch.common.xcontent.XContentParserUtils.ensureExpectedToken; import static org.elasticsearch.search.aggregations.AggregationBuilders.dateRange; import static org.elasticsearch.search.aggregations.AggregatorFactories.VALID_AGG_NAME; @@ -33,8 +34,13 @@ import org.apache.logging.log4j.Logger; import org.apache.lucene.search.join.ScoreMode; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.ResourceNotFoundException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.ParsingException; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; @@ -60,6 +66,8 @@ import com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetector; import com.amazon.opendistroforelasticsearch.ad.model.Feature; import com.amazon.opendistroforelasticsearch.ad.model.FeatureData; +import com.amazon.opendistroforelasticsearch.ad.rest.handler.AnomalyDetectorFunction; +import com.amazon.opendistroforelasticsearch.ad.transport.GetAnomalyDetectorResponse; import com.amazon.opendistroforelasticsearch.commons.ConfigConstants; import com.amazon.opendistroforelasticsearch.commons.authuser.User; @@ -450,6 +458,132 @@ public static User getUserContext(Client client) { return User.parse(userStr); } + public static void resolveUserAndExecute( + User requestedUser, + String detectorId, + boolean filterByEnabled, + ActionListener listener, + AnomalyDetectorFunction function, + Client client, + ClusterService clusterService, + NamedXContentRegistry xContentRegistry + ) { + if (requestedUser == null) { + // Security is disabled or user is superadmin + function.execute(); + } else if (!filterByEnabled) { + // security is enabled and filterby is disabled. + function.execute(); + } else { + // security is enabled and filterby is enabled. + // Get detector and check if the user has permissions to access the detector + try { + getDetector(requestedUser, detectorId, listener, function, client, clusterService, xContentRegistry); + } catch (Exception e) { + listener.onFailure(e); + } + } + } + + public static void getDetector( + User requestUser, + String detectorId, + ActionListener listener, + AnomalyDetectorFunction function, + Client client, + ClusterService clusterService, + NamedXContentRegistry xContentRegistry + ) { + if (clusterService.state().metadata().indices().containsKey(AnomalyDetector.ANOMALY_DETECTORS_INDEX)) { + GetRequest request = new GetRequest(AnomalyDetector.ANOMALY_DETECTORS_INDEX).id(detectorId); + client + .get( + request, + ActionListener + .wrap( + response -> onGetAdResponse(response, requestUser, detectorId, listener, function, xContentRegistry), + exception -> { + logger.error("Failed to get anomaly detector: " + detectorId, exception); + listener.onFailure(exception); + } + ) + ); + } else { + listener + .onFailure( + new ResourceNotFoundException("Failed to find anomaly detector index: " + AnomalyDetector.ANOMALY_DETECTORS_INDEX) + ); + } + } + + public static void onGetAdResponse( + GetResponse response, + User requestUser, + String detectorId, + ActionListener listener, + AnomalyDetectorFunction function, + NamedXContentRegistry xContentRegistry + ) { + if (response.isExists()) { + try ( + XContentParser parser = RestHandlerUtils.createXContentParserFromRegistry(xContentRegistry, response.getSourceAsBytesRef()) + ) { + ensureExpectedToken(XContentParser.Token.START_OBJECT, parser.nextToken(), parser); + AnomalyDetector detector = AnomalyDetector.parse(parser); + User resourceUser = detector.getUser(); + + if (checkUserPermissions(requestUser, resourceUser, detectorId)) { + function.execute(); + } else { + logger.debug("User: " + requestUser.getName() + " does not have permissions to access detector: " + detectorId); + listener.onFailure(new ElasticsearchException("User does not have permissions to access detector: " + detectorId)); + } + } catch (Exception e) { + listener.onFailure(new ElasticsearchException("Unable to get user information from detector " + detectorId)); + } + } else { + listener.onFailure(new ResourceNotFoundException("Could not find detector " + detectorId)); + } + } + + private static boolean checkUserPermissions(User requestedUser, User resourceUser, String detectorId) throws Exception { + if (resourceUser.getBackendRoles() == null || requestedUser.getBackendRoles() == null) { + return false; + } + // Check if requested user has backend role required to access the resource + for (String backendRole : requestedUser.getBackendRoles()) { + if (resourceUser.getBackendRoles().contains(backendRole)) { + logger + .debug( + "User: " + + requestedUser.getName() + + " has backend role: " + + backendRole + + " permissions to access detector: " + + detectorId + ); + return true; + } + } + return false; + } + + public static boolean checkFilterByBackendRoles(User requestedUser, ActionListener listener) { + if (requestedUser == null) { + return false; + } + if (requestedUser.getBackendRoles().isEmpty()) { + listener + .onFailure( + new ElasticsearchException( + "Filter by backend roles is enabled and User " + requestedUser.getName() + " does not have backend roles configured" + ) + ); + return false; + } + return true; + } + /** * Parse max timestamp aggregation named CommonName.AGG_NAME_MAX * @param searchResponse Search response diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobActionTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobActionTests.java index 26ac5859..9c519f8b 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobActionTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/AnomalyDetectorJobActionTests.java @@ -16,22 +16,34 @@ package com.amazon.opendistroforelasticsearch.ad.transport; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.amazon.opendistroforelasticsearch.ad.indices.AnomalyDetectionIndices; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; +import com.amazon.opendistroforelasticsearch.commons.ConfigConstants; public class AnomalyDetectorJobActionTests extends ESIntegTestCase { private AnomalyDetectorJobTransportAction action; @@ -43,11 +55,26 @@ public class AnomalyDetectorJobActionTests extends ESIntegTestCase { @Before public void setUp() throws Exception { super.setUp(); + ClusterService clusterService = mock(ClusterService.class); + ClusterSettings clusterSettings = new ClusterSettings( + Settings.EMPTY, + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES))) + ); + + Settings build = Settings.builder().build(); + ThreadContext threadContext = new ThreadContext(build); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, "alice|odfe,aes|engineering,operations"); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); + Client client = mock(Client.class); + org.elasticsearch.threadpool.ThreadPool mockThreadPool = mock(ThreadPool.class); + when(client.threadPool()).thenReturn(mockThreadPool); + when(mockThreadPool.getThreadContext()).thenReturn(threadContext); action = new AnomalyDetectorJobTransportAction( mock(TransportService.class), mock(ActionFilters.class), - client(), + client, + clusterService, indexSettings(), mock(AnomalyDetectionIndices.class), xContentRegistry() @@ -58,7 +85,7 @@ public void setUp() throws Exception { @Override public void onResponse(AnomalyDetectorJobResponse adResponse) { // Will not be called as there is no detector - Assert.assertTrue(true); + Assert.assertTrue(false); } @Override diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorActionTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorActionTests.java index 6a3ab3cb..6e7a1e61 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorActionTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/DeleteAnomalyDetectorActionTests.java @@ -16,15 +16,22 @@ package com.amazon.opendistroforelasticsearch.ad.transport; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.action.delete.DeleteResponse; import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.transport.TransportService; @@ -32,6 +39,8 @@ import org.junit.Before; import org.junit.Test; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; + public class DeleteAnomalyDetectorActionTests extends ESIntegTestCase { private DeleteAnomalyDetectorTransportAction action; private ActionListener response; @@ -40,11 +49,18 @@ public class DeleteAnomalyDetectorActionTests extends ESIntegTestCase { @Before public void setUp() throws Exception { super.setUp(); + ClusterService clusterService = mock(ClusterService.class); + ClusterSettings clusterSettings = new ClusterSettings( + Settings.EMPTY, + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES))) + ); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); action = new DeleteAnomalyDetectorTransportAction( mock(TransportService.class), mock(ActionFilters.class), client(), - clusterService(), + clusterService, + Settings.EMPTY, xContentRegistry() ); response = new ActionListener() { diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTests.java index c07ba8a0..9a96e17d 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTests.java @@ -23,7 +23,9 @@ import java.io.IOException; import java.security.InvalidParameterException; +import java.util.Arrays; import java.util.Collections; +import java.util.HashSet; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.get.GetRequest; @@ -31,6 +33,8 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportService; @@ -39,6 +43,7 @@ import com.amazon.opendistroforelasticsearch.ad.AbstractADTest; import com.amazon.opendistroforelasticsearch.ad.constant.CommonErrorMessages; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; import com.amazon.opendistroforelasticsearch.ad.util.DiscoveryNodeFilterer; public class GetAnomalyDetectorTests extends AbstractADTest { @@ -67,6 +72,12 @@ public static void tearDownAfterClass() { @Override public void setUp() throws Exception { super.setUp(); + ClusterService clusterService = mock(ClusterService.class); + ClusterSettings clusterSettings = new ClusterSettings( + Settings.EMPTY, + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES))) + ); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); transportService = new TransportService( Settings.EMPTY, @@ -85,7 +96,15 @@ public void setUp() throws Exception { client = mock(Client.class); when(client.threadPool()).thenReturn(threadPool); - action = new GetAnomalyDetectorTransportAction(transportService, nodeFilter, actionFilters, client, xContentRegistry()); + action = new GetAnomalyDetectorTransportAction( + transportService, + nodeFilter, + actionFilters, + clusterService, + client, + Settings.EMPTY, + xContentRegistry() + ); } public void testInvalidRequest() throws IOException { diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportActionTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportActionTests.java index d4254729..7608e2f9 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportActionTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/GetAnomalyDetectorTransportActionTests.java @@ -15,16 +15,25 @@ package com.amazon.opendistroforelasticsearch.ad.transport; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + import java.io.IOException; import java.time.Instant; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import java.util.Map; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.NamedWriteableAwareStreamInput; import org.elasticsearch.common.io.stream.NamedWriteableRegistry; import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.ToXContent; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.rest.RestStatus; @@ -40,6 +49,7 @@ import com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetector; import com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetectorJob; import com.amazon.opendistroforelasticsearch.ad.model.EntityProfile; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; import com.amazon.opendistroforelasticsearch.ad.util.DiscoveryNodeFilterer; import com.amazon.opendistroforelasticsearch.ad.util.RestHandlerUtils; import com.google.common.collect.ImmutableMap; @@ -53,11 +63,19 @@ public class GetAnomalyDetectorTransportActionTests extends ESSingleNodeTestCase @Before public void setUp() throws Exception { super.setUp(); + ClusterService clusterService = mock(ClusterService.class); + ClusterSettings clusterSettings = new ClusterSettings( + Settings.EMPTY, + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES))) + ); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); action = new GetAnomalyDetectorTransportAction( Mockito.mock(TransportService.class), Mockito.mock(DiscoveryNodeFilterer.class), Mockito.mock(ActionFilters.class), + clusterService, client(), + Settings.EMPTY, xContentRegistry() ); task = Mockito.mock(Task.class); diff --git a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportActionTests.java b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportActionTests.java index c2e4b644..18f01e73 100644 --- a/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportActionTests.java +++ b/src/test/java/com/amazon/opendistroforelasticsearch/ad/transport/IndexAnomalyDetectorTransportActionTests.java @@ -16,14 +16,25 @@ package com.amazon.opendistroforelasticsearch.ad.transport; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.WriteRequest; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.settings.ClusterSettings; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESIntegTestCase; +import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.TransportService; import org.junit.Assert; import org.junit.Before; @@ -31,23 +42,33 @@ import com.amazon.opendistroforelasticsearch.ad.indices.AnomalyDetectionIndices; import com.amazon.opendistroforelasticsearch.ad.model.AnomalyDetector; +import com.amazon.opendistroforelasticsearch.ad.settings.AnomalyDetectorSettings; +import com.amazon.opendistroforelasticsearch.commons.ConfigConstants; public class IndexAnomalyDetectorTransportActionTests extends ESIntegTestCase { private IndexAnomalyDetectorTransportAction action; private Task task; private IndexAnomalyDetectorRequest request; private ActionListener response; + private ClusterService clusterService; + private ClusterSettings clusterSettings; @Override @Before public void setUp() throws Exception { super.setUp(); + clusterService = mock(ClusterService.class); + clusterSettings = new ClusterSettings( + Settings.EMPTY, + Collections.unmodifiableSet(new HashSet<>(Arrays.asList(AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES))) + ); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); action = new IndexAnomalyDetectorTransportAction( mock(TransportService.class), mock(ActionFilters.class), client(), - clusterService(), + clusterService, indexSettings(), mock(AnomalyDetectionIndices.class), xContentRegistry() @@ -68,7 +89,8 @@ public void setUp() throws Exception { response = new ActionListener() { @Override public void onResponse(IndexAnomalyDetectorResponse indexResponse) { - Assert.assertTrue(true); + // onResponse will not be called as we do not have the AD index + Assert.assertTrue(false); } @Override @@ -83,6 +105,52 @@ public void testIndexTransportAction() { action.doExecute(task, request, response); } + @Test + public void testIndexTransportActionWithUserAndFilterOn() { + Settings settings = Settings.builder().put(AnomalyDetectorSettings.FILTER_BY_BACKEND_ROLES.getKey(), true).build(); + ThreadContext threadContext = new ThreadContext(settings); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, "alice|odfe,aes|engineering,operations"); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); + Client client = mock(Client.class); + org.elasticsearch.threadpool.ThreadPool mockThreadPool = mock(ThreadPool.class); + when(client.threadPool()).thenReturn(mockThreadPool); + when(mockThreadPool.getThreadContext()).thenReturn(threadContext); + + IndexAnomalyDetectorTransportAction transportAction = new IndexAnomalyDetectorTransportAction( + mock(TransportService.class), + mock(ActionFilters.class), + client, + clusterService, + settings, + mock(AnomalyDetectionIndices.class), + xContentRegistry() + ); + transportAction.doExecute(task, request, response); + } + + @Test + public void testIndexTransportActionWithUserAndFilterOff() { + Settings settings = Settings.builder().build(); + ThreadContext threadContext = new ThreadContext(settings); + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER_INFO_THREAD_CONTEXT, "alice|odfe,aes|engineering,operations"); + when(clusterService.getClusterSettings()).thenReturn(clusterSettings); + Client client = mock(Client.class); + org.elasticsearch.threadpool.ThreadPool mockThreadPool = mock(ThreadPool.class); + when(client.threadPool()).thenReturn(mockThreadPool); + when(mockThreadPool.getThreadContext()).thenReturn(threadContext); + + IndexAnomalyDetectorTransportAction transportAction = new IndexAnomalyDetectorTransportAction( + mock(TransportService.class), + mock(ActionFilters.class), + client, + clusterService, + settings, + mock(AnomalyDetectionIndices.class), + xContentRegistry() + ); + transportAction.doExecute(task, request, response); + } + @Test public void testIndexDetectorAction() { Assert.assertNotNull(IndexAnomalyDetectorAction.INSTANCE.name());