diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportAcknowledgeAlertsAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportAcknowledgeAlertsAction.java index c56ce686c..59a0fc39e 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportAcknowledgeAlertsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportAcknowledgeAlertsAction.java @@ -7,15 +7,20 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.opensearch.OpenSearchException; +import org.opensearch.OpenSearchStatusException; import org.opensearch.action.ActionListener; import org.opensearch.action.StepListener; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.NamedXContentRegistry; import org.opensearch.commons.alerting.action.GetAlertsResponse; import org.opensearch.commons.alerting.model.Table; +import org.opensearch.commons.authuser.User; +import org.opensearch.rest.RestStatus; import org.opensearch.securityanalytics.action.AckAlertsRequest; import org.opensearch.securityanalytics.action.AckAlertsResponse; import org.opensearch.securityanalytics.action.AckAlertsAction; @@ -23,28 +28,51 @@ import org.opensearch.securityanalytics.action.GetDetectorResponse; import org.opensearch.securityanalytics.alerts.AlertsService; import org.opensearch.securityanalytics.model.Detector; +import org.opensearch.securityanalytics.settings.SecurityAnalyticsSettings; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; -public class TransportAcknowledgeAlertsAction extends HandledTransportAction { +public class TransportAcknowledgeAlertsAction extends HandledTransportAction implements SecureTransportAction { private final TransportGetDetectorAction transportGetDetectorAction; private final NamedXContentRegistry xContentRegistry; + private final ClusterService clusterService; + + private final Settings settings; + + private final ThreadPool threadPool; private final AlertsService alertsService; + private volatile Boolean filterByEnabled; + private static final Logger log = LogManager.getLogger(TransportAcknowledgeAlertsAction.class); @Inject - public TransportAcknowledgeAlertsAction(TransportService transportService, ActionFilters actionFilters, TransportGetDetectorAction transportGetDetectorAction, NamedXContentRegistry xContentRegistry, Client client) { + public TransportAcknowledgeAlertsAction(TransportService transportService, ActionFilters actionFilters, ClusterService clusterService, ThreadPool threadPool, Settings settings, TransportGetDetectorAction transportGetDetectorAction, NamedXContentRegistry xContentRegistry, Client client) { super(AckAlertsAction.NAME, transportService, actionFilters, AckAlertsRequest::new); this.transportGetDetectorAction = transportGetDetectorAction; this.xContentRegistry = xContentRegistry; + this.clusterService = clusterService; + this.threadPool = threadPool; + this.settings = settings; + this.filterByEnabled = SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES.get(this.settings); this.alertsService = new AlertsService(client); + this.clusterService.getClusterSettings().addSettingsUpdateConsumer(SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES, this::setFilterByEnabled); } @Override protected void doExecute(Task task, AckAlertsRequest request, ActionListener actionListener) { + + User user = readUserFromThreadContext(this.threadPool); + + String validateBackendRoleMessage = validateUserBackendRoles(user, this.filterByEnabled); + if (!"".equals(validateBackendRoleMessage)) { + actionListener.onFailure(new OpenSearchStatusException("Do not have permissions to resource", RestStatus.FORBIDDEN)); + return; + } + GetDetectorRequest getDetectorRequest = new GetDetectorRequest(request.getDetectorId(), -3L); transportGetDetectorAction.doExecute(task, getDetectorRequest, new ActionListener() { @Override @@ -76,4 +104,8 @@ private boolean isDetectorAlertsMonitorMismatch(Detector detector, GetAlertsResp return getAlertsResponse.getAlerts().stream() .anyMatch(alert -> false == detector.getMonitorIds().contains(alert.getMonitorId())) ; } + + private void setFilterByEnabled(boolean filterByEnabled) { + this.filterByEnabled = filterByEnabled; + } } diff --git a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetAlertsAction.java b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetAlertsAction.java index 454eb09d4..cb17f0285 100644 --- a/src/main/java/org/opensearch/securityanalytics/transport/TransportGetAlertsAction.java +++ b/src/main/java/org/opensearch/securityanalytics/transport/TransportGetAlertsAction.java @@ -10,16 +10,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.lucene.search.join.ScoreMode; +import org.opensearch.OpenSearchStatusException; import org.opensearch.action.ActionListener; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.action.support.ActionFilters; import org.opensearch.action.support.HandledTransportAction; import org.opensearch.client.Client; +import org.opensearch.cluster.service.ClusterService; import org.opensearch.common.inject.Inject; +import org.opensearch.common.settings.Settings; import org.opensearch.common.xcontent.NamedXContentRegistry; +import org.opensearch.commons.authuser.User; import org.opensearch.index.query.NestedQueryBuilder; import org.opensearch.index.query.QueryBuilders; +import org.opensearch.rest.RestStatus; import org.opensearch.search.builder.SearchSourceBuilder; import org.opensearch.securityanalytics.action.GetAlertsAction; import org.opensearch.securityanalytics.action.GetAlertsRequest; @@ -27,33 +32,57 @@ import org.opensearch.securityanalytics.action.SearchDetectorRequest; import org.opensearch.securityanalytics.alerts.AlertsService; import org.opensearch.securityanalytics.model.Detector; +import org.opensearch.securityanalytics.settings.SecurityAnalyticsSettings; import org.opensearch.securityanalytics.util.DetectorUtils; import org.opensearch.tasks.Task; +import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; import static org.opensearch.securityanalytics.util.DetectorUtils.DETECTOR_TYPE_PATH; -public class TransportGetAlertsAction extends HandledTransportAction { +public class TransportGetAlertsAction extends HandledTransportAction implements SecureTransportAction { private final TransportSearchDetectorAction transportSearchDetectorAction; private final NamedXContentRegistry xContentRegistry; + private final ClusterService clusterService; + + private final Settings settings; + + private final ThreadPool threadPool; + private final AlertsService alertsService; + private volatile Boolean filterByEnabled; + private static final Logger log = LogManager.getLogger(TransportGetAlertsAction.class); @Inject - public TransportGetAlertsAction(TransportService transportService, ActionFilters actionFilters, TransportSearchDetectorAction transportSearchDetectorAction, NamedXContentRegistry xContentRegistry, Client client) { + public TransportGetAlertsAction(TransportService transportService, ActionFilters actionFilters, ClusterService clusterService, TransportSearchDetectorAction transportSearchDetectorAction, ThreadPool threadPool, Settings settings, NamedXContentRegistry xContentRegistry, Client client) { super(GetAlertsAction.NAME, transportService, actionFilters, GetAlertsRequest::new); this.transportSearchDetectorAction = transportSearchDetectorAction; this.xContentRegistry = xContentRegistry; this.alertsService = new AlertsService(client); + this.clusterService = clusterService; + this.threadPool = threadPool; + this.settings = settings; + this.filterByEnabled = SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES.get(this.settings); + this.clusterService.getClusterSettings().addSettingsUpdateConsumer(SecurityAnalyticsSettings.FILTER_BY_BACKEND_ROLES, this::setFilterByEnabled); } @Override protected void doExecute(Task task, GetAlertsRequest request, ActionListener actionListener) { + + User user = readUserFromThreadContext(this.threadPool); + + String validateBackendRoleMessage = validateUserBackendRoles(user, this.filterByEnabled); + if (!"".equals(validateBackendRoleMessage)) { + actionListener.onFailure(new OpenSearchStatusException("Do not have permissions to resource", RestStatus.FORBIDDEN)); + return; + } + if (request.getDetectorType() == null) { alertsService.getAlertsByDetectorId( request.getDetectorId(), @@ -109,4 +138,7 @@ public void onFailure(Exception e) { } } + private void setFilterByEnabled(boolean filterByEnabled) { + this.filterByEnabled = filterByEnabled; + } } \ No newline at end of file diff --git a/src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java index 6aa4e1a6b..6039f16ea 100644 --- a/src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/alerts/SecureAlertsRestApiIT.java @@ -43,6 +43,7 @@ public class SecureAlertsRestApiIT extends SecurityAnalyticsRestTestCase { static String TEST_HR_BACKEND_ROLE = "HR"; static String TEST_IT_BACKEND_ROLE = "IT"; private final String user = "userAlert"; + private static final String[] EMPTY_ARRAY = new String[0]; private RestClient userClient; @Before @@ -185,9 +186,21 @@ public void testGetAlerts_byDetectorId_success() throws IOException { getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); getAlertsBody = asMap(getAlertsResponse); Assert.assertEquals(1, getAlertsBody.get("total_alerts")); - userReadOnlyClient.close(); - deleteUser(userRead); + + // update user with no backend roles and try again + createUser(userRead, userRead, EMPTY_ARRAY); + userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + try { + getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + } catch (ResponseException e) + { + assertEquals("Get alert failed", RestStatus.FORBIDDEN, restStatus(e.getResponse())); + } + finally { + userReadOnlyClient.close(); + deleteUser(userRead); + } } @@ -282,10 +295,21 @@ public void testGetAlerts_byDetectorType_success() throws IOException, Interrupt getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); getAlertsBody = asMap(getAlertsResponse); Assert.assertEquals(1, getAlertsBody.get("total_alerts")); - userReadOnlyClient.close(); - deleteUser(userRead); + // update user with no backend roles and try again + createUser(userRead, userRead, EMPTY_ARRAY); + userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + try { + getAlertsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.ALERTS_BASE_URI, params, null); + } catch (ResponseException e) + { + assertEquals("Get alert failed", RestStatus.FORBIDDEN, restStatus(e.getResponse())); + } + finally { + userReadOnlyClient.close(); + deleteUser(userRead); + } } } \ No newline at end of file diff --git a/src/test/java/org/opensearch/securityanalytics/findings/SecureFindingRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/findings/SecureFindingRestApiIT.java index 6af7ecc0b..e95235bab 100644 --- a/src/test/java/org/opensearch/securityanalytics/findings/SecureFindingRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/findings/SecureFindingRestApiIT.java @@ -45,6 +45,7 @@ public class SecureFindingRestApiIT extends SecurityAnalyticsRestTestCase { static String TEST_HR_BACKEND_ROLE = "HR"; static String TEST_IT_BACKEND_ROLE = "IT"; private final String user = "userFinding"; + private static final String[] EMPTY_ARRAY = new String[0]; private RestClient userClient; @@ -141,9 +142,21 @@ public void testGetFindings_byDetectorId_success() throws IOException { getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); getFindingsBody = entityAsMap(getFindingsResponse); Assert.assertEquals(1, getFindingsBody.get("total_findings")); - userReadOnlyClient.close(); - deleteUser(userRead); + + // update user with no backend roles and try again + createUser(userRead, userRead, EMPTY_ARRAY); + userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + try { + getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + } catch (ResponseException e) + { + assertEquals("Get finding failed", RestStatus.FORBIDDEN, restStatus(e.getResponse())); + } + finally { + userReadOnlyClient.close(); + deleteUser(userRead); + } } @@ -280,8 +293,21 @@ public void testGetFindings_byDetectorType_success() throws IOException { getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); getFindingsBody = entityAsMap(getFindingsResponse); Assert.assertEquals(1, getFindingsBody.get("total_findings")); - userReadOnlyClient.close(); - deleteUser(userRead); + + + // update user with no backend roles and try again + createUser(userRead, userRead, EMPTY_ARRAY); + userReadOnlyClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userRead, userRead).setSocketTimeout(60000).build(); + try { + getFindingsResponse = makeRequest(userReadOnlyClient, "GET", SecurityAnalyticsPlugin.FINDINGS_BASE_URI + "/_search", params, null); + } catch (ResponseException e) + { + assertEquals("Get finding failed", RestStatus.FORBIDDEN, restStatus(e.getResponse())); + } + finally { + userReadOnlyClient.close(); + deleteUser(userRead); + } } } diff --git a/src/test/java/org/opensearch/securityanalytics/resthandler/SecureDetectorRestApiIT.java b/src/test/java/org/opensearch/securityanalytics/resthandler/SecureDetectorRestApiIT.java index e4f760f93..ed111b2ed 100644 --- a/src/test/java/org/opensearch/securityanalytics/resthandler/SecureDetectorRestApiIT.java +++ b/src/test/java/org/opensearch/securityanalytics/resthandler/SecureDetectorRestApiIT.java @@ -155,10 +155,7 @@ public void testCreateDetectorWithFullAccess() throws IOException { Assert.assertEquals(createdId, getResponseBody.get("_id")); //Search on id should give one result - String queryJson = "{ \"query\": { \"match\": { \"_id\" : \"" + createdId + "\"} } }"; -// String queryJson = "{ \"query\": { \"match_all\": { } } }"; -// HttpEntity requestEntity = new NStringEntity(queryJson, ContentType.APPLICATION_JSON); Response searchResponse = makeRequest(userReadOnlyClient, "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI + "/" + "_search", Collections.emptyMap(), requestEntity); Map searchResponseBody = asMap(searchResponse); @@ -170,4 +167,41 @@ public void testCreateDetectorWithFullAccess() throws IOException { userReadOnlyClient.close(); deleteUser(userRead); } + + public void testCreateDetectorWithNoBackendRoles() throws IOException { + // try to do create detector as a user with no backend roles + String userFull= "userFull"; + String[] backendRoles = {}; + createUserWithData( userFull, userFull, SECURITY_ANALYTICS_FULL_ACCESS_ROLE, backendRoles ); + RestClient userFullClient = new SecureRestClientBuilder(getClusterHosts().toArray(new HttpHost[]{}), isHttps(), userFull, userFull).setSocketTimeout(60000).build(); + + String index = createTestIndex(client(), randomIndex(), windowsIndexMapping(), Settings.EMPTY); + + // Execute CreateMappingsAction to add alias mapping for index + Request createMappingRequest = new Request("POST", SecurityAnalyticsPlugin.MAPPER_BASE_URI); + // both req params and req body are supported + createMappingRequest.setJsonEntity( + "{ \"index_name\":\"" + index + "\"," + + " \"rule_topic\":\"" + randomDetectorType() + "\", " + + " \"partial\":true" + + "}" + ); + + Response response = userFullClient.performRequest(createMappingRequest); + assertEquals(HttpStatus.SC_OK, response.getStatusLine().getStatusCode()); + + Detector detector = randomDetector(getRandomPrePackagedRules()); + // Enable backend filtering and try to read detector as a user with no backend roles matching the user who created the detector + enableOrDisableFilterBy("true"); + try { + Response createResponse = makeRequest(userFullClient, "POST", SecurityAnalyticsPlugin.DETECTOR_BASE_URI, Collections.emptyMap(), toHttpEntity(detector)); + } catch (ResponseException e) + { + assertEquals("Create detector failed", RestStatus.FORBIDDEN, restStatus(e.getResponse())); + } + finally { + userFullClient.close(); + deleteUser(userFull); + } + } } \ No newline at end of file