diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/Search.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/Search.java index a284393ba247..edd289a1b058 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/Search.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/Search.java @@ -28,6 +28,7 @@ import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.common.graph.MutableGraph; +import org.graylog.plugins.views.search.engine.validation.DataWarehouseSearchValidator; import org.graylog.plugins.views.search.permissions.StreamPermissions; import org.graylog.plugins.views.search.rest.ExecutionState; import org.graylog.plugins.views.search.views.PluginMetadataSummary; @@ -133,7 +134,7 @@ public Search applyExecutionState(final ExecutionState executionState) { public Search addStreamsToQueriesWithoutStreams(Supplier> defaultStreamsSupplier) { - if (!hasQueriesWithoutStreams()) { + if (!hasQueriesWithoutStreams() || DataWarehouseSearchValidator.containsDataWarehouseSearchElements(this)) { return this; } final Set withStreams = queries().stream().filter(Query::hasStreams).collect(toSet()); @@ -157,7 +158,7 @@ public Search addStreamsToQueriesWithoutStreams(Supplier> defaultStr public Search addStreamsToQueriesWithCategories(Function, Stream> categoryMappingFunction, StreamPermissions streamPermissions) { - if (!hasQueriesWithStreamCategories()) { + if (!hasQueriesWithStreamCategories() || DataWarehouseSearchValidator.containsDataWarehouseSearchElements(this)) { return this; } final Set withStreamCategories = queries().stream().filter(q -> !q.usedStreamCategories().isEmpty()).collect(toSet()); @@ -178,7 +179,7 @@ public Search addStreamsToQueriesWithCategories(Function, Str public Search addStreamsToSearchTypesWithCategories(Function, Stream> categoryMappingFunction, StreamPermissions streamPermissions) { - if (!hasQuerySearchTypesWithStreamCategories()) { + if (!hasQuerySearchTypesWithStreamCategories() || DataWarehouseSearchValidator.containsDataWarehouseSearchElements(this)) { return this; } final Set withStreamCategories = queries().stream() diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/SearchType.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/SearchType.java index 5a6e815460ef..e96c24f29ff0 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/SearchType.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/SearchType.java @@ -25,6 +25,7 @@ import org.graylog.plugins.views.search.engine.BackendQuery; import org.graylog.plugins.views.search.rest.SearchTypeExecutionState; import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; +import org.graylog.plugins.views.search.searchtypes.SearchEngineSearchType; import org.graylog.plugins.views.search.timeranges.DerivedTimeRange; import org.graylog2.contentpacks.ContentPackable; import org.graylog2.contentpacks.EntityDescriptorIds; @@ -140,7 +141,7 @@ interface Result { } @JsonAutoDetect - class Fallback implements SearchType { + class Fallback implements SearchEngineSearchType { @JsonProperty private String type; diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/elasticsearch/ElasticsearchQueryString.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/elasticsearch/ElasticsearchQueryString.java index a1bf4411abf1..c5afea8765e0 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/elasticsearch/ElasticsearchQueryString.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/elasticsearch/ElasticsearchQueryString.java @@ -23,13 +23,14 @@ import com.fasterxml.jackson.annotation.JsonTypeName; import com.google.auto.value.AutoValue; import com.google.common.base.Strings; +import jakarta.validation.constraints.NotNull; +import org.graylog.plugins.views.search.SearchType; import org.graylog.plugins.views.search.engine.BackendQuery; +import org.graylog.plugins.views.search.searchtypes.SearchEngineSearchType; import javax.annotation.Nullable; - -import jakarta.validation.constraints.NotNull; - import java.util.Optional; +import java.util.Set; @AutoValue @JsonAutoDetect @@ -65,6 +66,11 @@ public static ElasticsearchQueryString create(final @JsonProperty("type") String @Override public abstract String queryString(); + @JsonIgnore + public Set> supportedSearchTypes() { + return Set.of(SearchEngineSearchType.class); + } + @JsonIgnore public boolean isEmpty() { String trimmed = queryString().trim(); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/BackendQuery.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/BackendQuery.java index f53d3d62967c..0c35d546ac53 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/BackendQuery.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/BackendQuery.java @@ -16,9 +16,13 @@ */ package org.graylog.plugins.views.search.engine; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonTypeInfo; +import org.graylog.plugins.views.search.SearchType; import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; +import java.util.Set; + @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = BackendQuery.TYPE_FIELD, @@ -30,4 +34,7 @@ public interface BackendQuery { String type(); String queryString(); + + @JsonIgnore + Set> supportedSearchTypes(); } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/EngineBindings.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/EngineBindings.java index 0c45326af3b6..602446e982af 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/EngineBindings.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/EngineBindings.java @@ -20,7 +20,9 @@ import org.graylog.plugins.views.search.engine.normalization.DecorateQueryStringsNormalizer; import org.graylog.plugins.views.search.engine.normalization.PluggableSearchNormalization; import org.graylog.plugins.views.search.engine.normalization.SearchNormalization; +import org.graylog.plugins.views.search.engine.validation.DataWarehouseSearchValidator; import org.graylog.plugins.views.search.engine.validation.PluggableSearchValidation; +import org.graylog.plugins.views.search.engine.validation.SearchTypesMatchBackendQueryValidator; import org.graylog.plugins.views.search.engine.validation.SearchValidation; import org.graylog.plugins.views.search.engine.validation.TimeRangeValidator; @@ -34,5 +36,8 @@ protected void configure() { // Triggering set binder explicitly, so no injection errors are being caused if no implementation is bound. searchPostValidationNormalizerBinder(); registerSearchValidator(TimeRangeValidator.class); + registerSearchValidator(SearchTypesMatchBackendQueryValidator.class); + //while DW searches are enterprise only, the validation should probably be present in gl-server, i.e. to discover invalid searches even if a client has lost the license + registerSearchValidator(DataWarehouseSearchValidator.class); } } diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/validation/DataWarehouseSearchValidator.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/validation/DataWarehouseSearchValidator.java new file mode 100644 index 000000000000..538676487c74 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/validation/DataWarehouseSearchValidator.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.engine.validation; + +import com.google.common.collect.ImmutableSet; +import org.graylog.plugins.views.search.Query; +import org.graylog.plugins.views.search.Search; +import org.graylog.plugins.views.search.SearchType; +import org.graylog.plugins.views.search.errors.QueryError; +import org.graylog.plugins.views.search.errors.SearchError; +import org.graylog.plugins.views.search.errors.SearchTypeError; +import org.graylog.plugins.views.search.permissions.SearchUser; +import org.graylog.plugins.views.search.searchtypes.DataWarehouseSearchType; + +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +public class DataWarehouseSearchValidator implements SearchValidator { + + @Override + public Set validate(final Search search, + final SearchUser searchUser) { + //this should be either validated elsewhere or impossible + assert search.queries() != null; + assert !search.queries().isEmpty(); + + if (containsDataWarehouseSearchElements(search)) { + if (search.queries().size() > 1) { + return wholeSearchInvalid(search, "Data Warehouse elements present in Search, only 1 query allowed for those type of searches"); + } + return validate(search.queries().stream().findFirst().get(), searchUser); + } else { + return Set.of(); + } + } + + @Override + public Set validate(final Query query, + final SearchUser searchUser) { + if (containsDataWarehouseSearchElements(query)) { + final ImmutableSet searchTypes = query.searchTypes(); + if (searchTypes.size() != 1) { + return Set.of(new QueryError(query, "Data Warehouse query can contain only one search type")); + } + final Optional first = searchTypes.stream().findFirst(); + + final Set streams = first.get().streams(); + if (streams == null || streams.size() > 1) { + return Set.of(new SearchTypeError(query, first.get().id(), + "Data Warehouse preview can be executed on only 1 stream, search type contained more")); + } + + } + return Set.of(); + } + + public static boolean containsDataWarehouseSearchElements(final Search search) { + return search.queries().stream().anyMatch(DataWarehouseSearchValidator::containsDataWarehouseSearchElements); + } + + public static boolean containsDataWarehouseSearchElements(final Query query) { + return (query.query().type().startsWith(DataWarehouseSearchType.PREFIX)) + || (query.searchTypes().stream().anyMatch(searchType -> searchType instanceof DataWarehouseSearchType)); + } + + private Set wholeSearchInvalid(final Search search, final String explanation) { + return search.queries() + .stream() + .map(query -> new QueryError(query, explanation)) + .collect(Collectors.toSet()); + } +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/validation/SearchTypesMatchBackendQueryValidator.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/validation/SearchTypesMatchBackendQueryValidator.java new file mode 100644 index 000000000000..b41c7bc7e6f6 --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/engine/validation/SearchTypesMatchBackendQueryValidator.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.engine.validation; + +import org.graylog.plugins.views.search.Query; +import org.graylog.plugins.views.search.Search; +import org.graylog.plugins.views.search.errors.SearchError; +import org.graylog.plugins.views.search.errors.SearchTypeError; +import org.graylog.plugins.views.search.permissions.SearchUser; + +import java.util.Set; +import java.util.stream.Collectors; + +public class SearchTypesMatchBackendQueryValidator implements SearchValidator { + + public static final String INVALID_SEARCH_TYPE_FOR_GIVEN_QUERY_TYPE_MSG = "Invalid search type for given query type"; + + @Override + public Set validate(final Search search, + final SearchUser searchUser) { + return search.queries() + .stream() + .flatMap(query -> validate(query, searchUser).stream()) + .collect(Collectors.toSet()); + } + + @Override + public Set validate(final Query query, + final SearchUser searchUser) { + return query.searchTypes().stream() + .filter(searchType -> query.query() + .supportedSearchTypes() + .stream() + .anyMatch(supportedType -> !supportedType.isAssignableFrom(searchType.getClass()))) + .map(searchType -> new SearchTypeError(query, searchType.id(), INVALID_SEARCH_TYPE_FOR_GIVEN_QUERY_TYPE_MSG)) + .collect(Collectors.toSet()); + } +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/DataWarehouseSearchType.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/DataWarehouseSearchType.java new file mode 100644 index 000000000000..ef6686c1710a --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/DataWarehouseSearchType.java @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.searchtypes; + +import org.graylog.plugins.views.search.SearchType; + +/** + * Marker interface for search types that are not search engine related, but Data Warehouse/Iceberg related + */ +public interface DataWarehouseSearchType extends SearchType { + + String PREFIX = "data_warehouse_"; +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java index f816c9d02262..de28fb9615aa 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/MessageList.java @@ -52,7 +52,7 @@ @AutoValue @JsonTypeName(MessageList.NAME) @JsonDeserialize(builder = MessageList.Builder.class) -public abstract class MessageList implements SearchType { +public abstract class MessageList implements SearchEngineSearchType { public static final String NAME = "messages"; @Override diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/SearchEngineSearchType.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/SearchEngineSearchType.java new file mode 100644 index 000000000000..5e0d6cf9e75d --- /dev/null +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/SearchEngineSearchType.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.searchtypes; + +import org.graylog.plugins.views.search.SearchType; + +/** + * Marker interface for search types that are search engine related. + */ +public interface SearchEngineSearchType extends SearchType { +} diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/events/EventList.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/events/EventList.java index c423edbe93bc..7d455232a71b 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/events/EventList.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/events/EventList.java @@ -29,6 +29,7 @@ import org.graylog.plugins.views.search.engine.BackendQuery; import org.graylog.plugins.views.search.rest.SearchTypeExecutionState; import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; +import org.graylog.plugins.views.search.searchtypes.SearchEngineSearchType; import org.graylog.plugins.views.search.timeranges.DerivedTimeRange; import org.graylog2.contentpacks.EntityDescriptorIds; import org.graylog2.contentpacks.model.entities.EntityDescriptor; @@ -52,7 +53,7 @@ @AutoValue @JsonTypeName(EventList.NAME) @JsonDeserialize(builder = EventList.Builder.class) -public abstract class EventList implements SearchType, HasAttributeFilter { +public abstract class EventList implements SearchEngineSearchType, HasAttributeFilter { public static final int DEFAULT_PAGE_SIZE = 10; public static final String NAME = "events"; public static final Set KNOWN_ATTRIBUTES = Set.of("priority", "event_definition_id", "alert"); diff --git a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java index c83b1c6def23..50fd55299580 100644 --- a/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java +++ b/graylog2-server/src/main/java/org/graylog/plugins/views/search/searchtypes/pivot/Pivot.java @@ -28,6 +28,7 @@ import org.graylog.plugins.views.search.engine.BackendQuery; import org.graylog.plugins.views.search.rest.SearchTypeExecutionState; import org.graylog.plugins.views.search.searchfilters.model.UsedSearchFilter; +import org.graylog.plugins.views.search.searchtypes.SearchEngineSearchType; import org.graylog.plugins.views.search.timeranges.DerivedTimeRange; import org.graylog2.contentpacks.EntityDescriptorIds; import org.graylog2.contentpacks.model.entities.EntityDescriptor; @@ -47,7 +48,7 @@ @AutoValue @JsonTypeName(Pivot.NAME) @JsonDeserialize(builder = Pivot.Builder.class) -public abstract class Pivot implements SearchType { +public abstract class Pivot implements SearchEngineSearchType { public static final String NAME = "pivot"; @Override diff --git a/graylog2-server/src/test/java/org/graylog/plugins/views/search/engine/validation/SearchTypesMatchBackendQueryValidatorTest.java b/graylog2-server/src/test/java/org/graylog/plugins/views/search/engine/validation/SearchTypesMatchBackendQueryValidatorTest.java new file mode 100644 index 000000000000..bf4ee92c5857 --- /dev/null +++ b/graylog2-server/src/test/java/org/graylog/plugins/views/search/engine/validation/SearchTypesMatchBackendQueryValidatorTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2020 Graylog, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Server Side Public License, version 1, + * as published by MongoDB, Inc. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * Server Side Public License for more details. + * + * You should have received a copy of the Server Side Public License + * along with this program. If not, see + * . + */ +package org.graylog.plugins.views.search.engine.validation; + +import org.graylog.plugins.views.search.Query; +import org.graylog.plugins.views.search.SearchType; +import org.graylog.plugins.views.search.elasticsearch.ElasticsearchQueryString; +import org.graylog.plugins.views.search.errors.SearchError; +import org.graylog.plugins.views.search.permissions.SearchUser; +import org.graylog.plugins.views.search.searchtypes.MessageList; +import org.graylog.plugins.views.search.searchtypes.pivot.Pivot; +import org.graylog.plugins.views.search.searchtypes.pivot.buckets.Values; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.graylog.plugins.views.search.engine.validation.SearchTypesMatchBackendQueryValidator.INVALID_SEARCH_TYPE_FOR_GIVEN_QUERY_TYPE_MSG; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +@ExtendWith(MockitoExtension.class) +class SearchTypesMatchBackendQueryValidatorTest { + + private SearchTypesMatchBackendQueryValidator toTest; + @Mock + private SearchUser searchUser; + + @BeforeEach + void setUp() { + toTest = new SearchTypesMatchBackendQueryValidator(); + } + + @Test + void testValidationIsOkOnProperSearchTypes() { + Query query = Query.builder() + .query(ElasticsearchQueryString.of("bayobongo")) + .searchTypes(Set.of( + MessageList.builder() + .id("messageListId") + .build(), + + Pivot.builder() + .rowGroups( + Values.builder() + .fields(List.of("id", "whatever")) + .limit(10) + .build() + ) + .series(List.of()) + .rollup(false) + .build() + )) + .build(); + + assertEquals(Set.of(), toTest.validate(query, searchUser)); + } + + @Test + void testValidationDiscoversWrongSearchTypes() { + final SearchType wrongSearchType = mock(SearchType.class); + doReturn("wrong!").when(wrongSearchType).id(); + Query query = Query.builder() + .id("query_id") + .query(ElasticsearchQueryString.of("bayobongo")) + .searchTypes(Set.of( + MessageList.builder() + .id("messageListId") + .build(), + + wrongSearchType + )) + .build(); + + final Set errors = toTest.validate(query, searchUser); + assertThat(errors) + .isNotNull() + .hasSize(1); + assertThat(errors.stream().findFirst().get().description()) + .isEqualTo(INVALID_SEARCH_TYPE_FOR_GIVEN_QUERY_TYPE_MSG); + + } +}