This is an automated email from the ASF dual-hosted git repository. pinal pushed a commit to branch atlas-2.5 in repository https://gitbox.apache.org/repos/asf/atlas.git
The following commit(s) were added to refs/heads/atlas-2.5 by this push: new 498412c24 ATLAS-5053: Add recursive validation in Basic Search API to fix regression issue (#373) 498412c24 is described below commit 498412c24d1f080aa34588b79ea805a87206af8d Author: Aditya Gupta <aditya.gu...@freestoneinfotech.com> AuthorDate: Tue Jul 15 11:50:32 2025 +0530 ATLAS-5053: Add recursive validation in Basic Search API to fix regression issue (#373) (cherry picked from commit cb1e53abb1451252064b7b8aa8e20a7e177a4da9) --- .../org/apache/atlas/web/rest/DiscoveryREST.java | 35 +++- .../atlas/web/integration/BasicSearchIT.java | 88 ++++++++- .../json/search-parameters/attribute-filters.json | 1 + .../json/search-parameters/attribute-name.json | 189 +++++++++++++++---- .../json/search-parameters/attribute-value.json | 202 +++++++++++++++++---- .../resources/json/search-parameters/operator.json | 196 ++++++++++++++------ .../json/search-parameters/tag-filters.json | 14 ++ 7 files changed, 583 insertions(+), 142 deletions(-) diff --git a/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java b/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java index 773b38765..f18600944 100644 --- a/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java +++ b/webapp/src/main/java/org/apache/atlas/web/rest/DiscoveryREST.java @@ -891,28 +891,43 @@ public class DiscoveryREST { } private void validateEntityFilter(SearchParameters parameters) throws AtlasBaseException { - FilterCriteria entityFilter = parameters.getEntityFilters(); + validateNestedCriteria(parameters.getEntityFilters()); + validateNestedCriteria(parameters.getTagFilters()); + } + + private void validateNestedCriteria(SearchParameters.FilterCriteria criteria) throws AtlasBaseException { + if (criteria == null) { + return; // Nothing to validate + } - if (entityFilter == null) { + boolean hasComposite = criteria.getCriterion() != null && !criteria.getCriterion().isEmpty(); + boolean hasLeaf = StringUtils.isNotEmpty(criteria.getAttributeName()) + || criteria.getOperator() != null + || StringUtils.isNotEmpty(criteria.getAttributeValue()); + + if (!hasComposite && !hasLeaf) { + // It's an empty filter object — skip (backward compatibility) return; } - if (entityFilter.getCriterion() != null && - !entityFilter.getCriterion().isEmpty()) { - if (entityFilter.getCondition() == null || StringUtils.isEmpty(entityFilter.getCondition().toString())) { + if (hasComposite) { + if (criteria.getCondition() == null || StringUtils.isEmpty(criteria.getCondition().toString())) { throw new AtlasBaseException("Condition (AND/OR) must be specified when using multiple filters."); } - for (FilterCriteria filterCriteria : entityFilter.getCriterion()) { - validateCriteria(filterCriteria); + for (FilterCriteria filterCriteria : criteria.getCriterion()) { + if (filterCriteria != null) { + validateNestedCriteria(filterCriteria); // Recursive check + } } } - else { - validateCriteria(entityFilter); + + if (hasLeaf) { + validateLeafFilterCriteria(criteria); } } - private void validateCriteria(SearchParameters.FilterCriteria criteria) throws AtlasBaseException { + private void validateLeafFilterCriteria(SearchParameters.FilterCriteria criteria) throws AtlasBaseException { if (criteria.getOperator() == null) { throw new AtlasBaseException(AtlasErrorCode.INVALID_OPERATOR, criteria.getAttributeName()); } diff --git a/webapp/src/test/java/org/apache/atlas/web/integration/BasicSearchIT.java b/webapp/src/test/java/org/apache/atlas/web/integration/BasicSearchIT.java index b851608f9..2709afdcb 100644 --- a/webapp/src/test/java/org/apache/atlas/web/integration/BasicSearchIT.java +++ b/webapp/src/test/java/org/apache/atlas/web/integration/BasicSearchIT.java @@ -35,6 +35,7 @@ import org.apache.atlas.model.typedef.AtlasClassificationDef; import org.apache.atlas.model.typedef.AtlasTypesDef; import org.apache.atlas.type.AtlasTypeUtil; import org.apache.atlas.utils.TestResourceFileUtils; +import org.apache.commons.lang.StringUtils; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -46,7 +47,6 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.function.Predicate; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.PUBLIC_ONLY; @@ -228,21 +228,33 @@ public class BasicSearchIT extends BaseResourceIT { } @Test - public void testAttributeSearchInvalidOperator() { - runNegativeSearchTest("search-parameters/operator", "ATLAS-400-00-103", parameters -> parameters.getEntityFilters() != null && parameters.getEntityFilters().getOperator() != null); + public void testComplexFilterInvalidOperator() { + runNegativeSearchTestForBasicSearch("search-parameters/operator", "ATLAS-400-00-103", + parameters -> (parameters.getEntityFilters() != null && containsAnyOperator(parameters.getEntityFilters())) || + (parameters.getTagFilters() != null && containsAnyOperator(parameters.getTagFilters()))); + runNegativeSearchTestForQuickSearch("search-parameters/operator", "ATLAS-400-00-103", + parameters -> parameters.getEntityFilters() != null && containsAnyOperator(parameters.getEntityFilters())); } @Test - public void testAttributeSearchEmptyNameAttribute() { - runNegativeSearchTest("search-parameters/attribute-name", "ATLAS-400-00-104", parameters -> parameters.getEntityFilters() != null && parameters.getEntityFilters().getAttributeName() != null); + public void testComplexFilterEmptyAttributeName() { + runNegativeSearchTestForBasicSearch("search-parameters/attribute-name", "ATLAS-400-00-104", + parameters -> (parameters.getEntityFilters() != null && containsAnyAttributeName(parameters.getEntityFilters())) || + (parameters.getTagFilters() != null && containsAnyAttributeName(parameters.getTagFilters()))); + runNegativeSearchTestForQuickSearch("search-parameters/attribute-name", "ATLAS-400-00-104", + parameters -> parameters.getEntityFilters() != null && containsAnyAttributeName(parameters.getEntityFilters())); } @Test - public void testAttributeSearchEmptyValueAttribute() { - runNegativeSearchTest("search-parameters/attribute-value", "ATLAS-400-00-105", parameters -> parameters.getEntityFilters() != null && parameters.getEntityFilters().getAttributeValue() != null); + public void testComplexFilterEmptyAttributeValue() { + runNegativeSearchTestForBasicSearch("search-parameters/attribute-value", "ATLAS-400-00-105", + parameters -> (parameters.getEntityFilters() != null && containsAnyAttributeValue(parameters.getEntityFilters())) || + (parameters.getTagFilters() != null && containsAnyAttributeValue(parameters.getTagFilters()))); + runNegativeSearchTestForQuickSearch("search-parameters/attribute-value", "ATLAS-400-00-105", + parameters -> parameters.getEntityFilters() != null && containsAnyAttributeValue(parameters.getEntityFilters())); } - public void runNegativeSearchTest(String jsonFile, String expectedErrorCode, java.util.function.Predicate<SearchParameters> paramFilter) { + public void runNegativeSearchTestForBasicSearch(String jsonFile, String expectedErrorCode, java.util.function.Predicate<SearchParameters> paramFilter) { try { BasicSearchParametersWithExpectation[] testExpectations = TestResourceFileUtils.readObjectFromJson(jsonFile, BasicSearchParametersWithExpectation[].class); assertNotNull(testExpectations); @@ -264,6 +276,66 @@ public class BasicSearchIT extends BaseResourceIT { } } + public void runNegativeSearchTestForQuickSearch(String jsonFile, String expectedErrorCode, java.util.function.Predicate<QuickSearchParameters> qspParamFilter) { + try { + BasicSearchParametersWithExpectation[] testExpectations = TestResourceFileUtils.readObjectFromJson(jsonFile, BasicSearchParametersWithExpectation[].class); + assertNotNull(testExpectations); + Arrays + .stream(testExpectations) + .map(testExpectation -> testExpectation.getSearchParameters()) + .map(sp -> { + QuickSearchParameters qsp = new QuickSearchParameters(); + qsp.setTypeName(sp.getTypeName()); + qsp.setEntityFilters(sp.getEntityFilters()); + return qsp; }) + .filter(qspParamFilter) + .forEach(params -> { + try { + atlasClientV2.quickSearch(params); + } + catch (AtlasServiceException e) { + assertTrue(e.getMessage().contains(expectedErrorCode), + "Expected error code " + expectedErrorCode + " in exception message: " + e.getMessage()); + } + }); + } catch (IOException e) { + fail(e.getMessage()); + } + } + + private boolean containsAnyMatchingCriteria(SearchParameters.FilterCriteria filterCriteria, java.util.function.Predicate<SearchParameters.FilterCriteria> matcher) { + if (filterCriteria == null) { + return false; + } + if (matcher.test(filterCriteria)) { + return true; + } + if (filterCriteria.getCriterion() != null) { + return filterCriteria.getCriterion().stream() + .filter(fc -> fc != null) + .anyMatch(fc -> containsAnyMatchingCriteria(fc, matcher)); + } + + return false; + } + + private boolean containsAnyOperator(SearchParameters.FilterCriteria filter) { + return containsAnyMatchingCriteria(filter, fc -> { + if (fc.getOperator() == null) { + return true; + } + return SearchParameters.Operator.fromString(fc.getOperator().toString()) == null; + }); + } + + private boolean containsAnyAttributeName(SearchParameters.FilterCriteria filter) { + return containsAnyMatchingCriteria(filter, fc -> StringUtils.isBlank(fc.getAttributeName())); + } + + private boolean containsAnyAttributeValue(SearchParameters.FilterCriteria filter) { + return containsAnyMatchingCriteria(filter, fc -> StringUtils.isBlank(fc.getAttributeValue())); + } + @Test(dependsOnMethods = "testSavedSearch") public void testExecuteSavedSearchByName() { try { diff --git a/webapp/src/test/resources/json/search-parameters/attribute-filters.json b/webapp/src/test/resources/json/search-parameters/attribute-filters.json index e90f203e3..2b4584c27 100644 --- a/webapp/src/test/resources/json/search-parameters/attribute-filters.json +++ b/webapp/src/test/resources/json/search-parameters/attribute-filters.json @@ -9,6 +9,7 @@ "offset": 0, "entityFilters": { "attributeName": "name", + "operator": "contains", "attributeValue": "testtable", "condition" : "OR", "criterion" : [ diff --git a/webapp/src/test/resources/json/search-parameters/attribute-name.json b/webapp/src/test/resources/json/search-parameters/attribute-name.json index 084b674f2..6baa0304f 100644 --- a/webapp/src/test/resources/json/search-parameters/attribute-name.json +++ b/webapp/src/test/resources/json/search-parameters/attribute-name.json @@ -1,34 +1,157 @@ -[ { - "testDescription": "hive_table contains testtable or retentionSize != 0", - "searchParameters": { - "typeName": "hive_table", - "excludeDeletedEntities": true, - "classification": "", - "query": "", - "limit": 25, - "offset": 0, - "entityFilters": { - "attributeName": "", - "attributeValue": "", - "condition" : "AND", - "criterion" : [ - { - "attributeName": "", - "operator": "eq", - "attributeValue": "testtable" - }, - { - "attributeName": "retention", - "operator": "neq", - "attributeValue": "0" - } - ] - }, - "tagFilters": null, - "attributes": [ - "" - ] +[ + { + "testDescription": "Invalid attribute name is empty", + "searchParameters": { + "typeName": "hive_table", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": { + "condition": "AND", + "criterion": [ + { + "attributeName": "", + "operator": "eq", + "attributeValue": "testtable" + }, + { + "attributeName": "", + "operator": "neq", + "attributeValue": "0" + } + ] + }, + "tagFilters": null, + "attributes": [""] + } }, - "expectedCount": 0 -} -] + { + "testDescription": "Nested invalid attribute names", + "searchParameters": { + "typeName": "hive_table", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "AND", + "criterion": [ + { + "attributeName": "", + "operator": "eq", + "attributeValue": "hive" + }, + { + "attributeName": "", + "operator": "neq", + "attributeValue": "hive" + } + ] + }, + { + "attributeName": "", + "operator": "neq", + "attributeValue": "" + } + ] + }, + "tagFilters": null, + "attributes": [""] + } + }, + { + "testDescription": "Only nested tagFilters with invalid (empty) attribute names", + "searchParameters": { + "typeName": "hive_column", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": null, + "tagFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "OR", + "criterion": [ + { + "attributeName": "", + "operator": "eq", + "attributeValue": "admin" + }, + { + "attributeName": "", + "operator": "neq", + "attributeValue": "test_value" + } + ] + }, + { + "attributeName": "", + "operator": "eq", + "attributeValue": "someValue" + } + ] + }, + "attributes": [""], + "classification": "_ALL_CLASSIFICATION_TYPES" + } + }, + { + "testDescription": "Nested entityFilters and tagFilters with invalid (empty) attribute names", + "searchParameters": { + "typeName": "hive_table", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "AND", + "criterion": [ + { + "attributeName": "", + "operator": "eq", + "attributeValue": "testtable" + }, + { + "attributeName": "", + "operator": "eq", + "attributeValue": "testuser" + } + ] + }, + { + "attributeName": "", + "operator": "neq", + "attributeValue": "0" + } + ] + }, + "tagFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "OR", + "criterion": [ + { + "attributeName": "", + "operator": "eq", + "attributeValue": "v1" + }, + { + "attributeName": "", + "operator": "eq", + "attributeValue": "someValue" + } + ] + } + ] + }, + "attributes": [""], + "classification": "_ALL_CLASSIFICATION_TYPES" + } + } +] \ No newline at end of file diff --git a/webapp/src/test/resources/json/search-parameters/attribute-value.json b/webapp/src/test/resources/json/search-parameters/attribute-value.json index 5bd367667..8b7ff7fb4 100644 --- a/webapp/src/test/resources/json/search-parameters/attribute-value.json +++ b/webapp/src/test/resources/json/search-parameters/attribute-value.json @@ -1,34 +1,172 @@ -[ { - "testDescription": "hive_table contains testtable or retentionSize != 0", - "searchParameters": { - "typeName": "hive_table", - "excludeDeletedEntities": true, - "classification": "", - "query": "", - "limit": 25, - "offset": 0, - "entityFilters": { - "attributeName": "name", - "attributeValue": "", - "condition" : "AND", - "criterion" : [ - { - "attributeName": "name", - "operator": "eq", - "attributeValue": "" - }, - { - "attributeName": "retention", - "operator": "neq", - "attributeValue": "" - } - ] - }, - "tagFilters": null, - "attributes": [ - "" - ] +[ + { + "testDescription": "Invalid attribute values: empty name and retention", + "searchParameters": { + "typeName": "hive_table", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": { + "condition": "AND", + "criterion": [ + { + "attributeName": "name", + "operator": "eq", + "attributeValue": "" + }, + { + "attributeName": "retention", + "operator": "neq", + "attributeValue": "" + } + ] + }, + "tagFilters": null, + "attributes": [] + } }, - "expectedCount": 0 -} + { + "testDescription": "Nested filters with empty values", + "searchParameters": { + "typeName": "hive_table", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "AND", + "criterion": [ + { + "attributeName": "name", + "operator": "eq", + "attributeValue": "" + }, + { + "attributeName": "owner", + "operator": "neq", + "attributeValue": "" + } + ] + }, + { + "attributeName": "retention", + "operator": "eq", + "attributeValue": "" + } + ] + }, + "tagFilters": null, + "attributes": [] + } + }, + { + "testDescription": "Nested tag filters with mixed operators and empty values", + "searchParameters": { + "typeName": "hive_column", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": null, + "tagFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "OR", + "criterion": [ + { + "attributeName": "__createdBy", + "operator": "eq", + "attributeValue": "" + }, + { + "attributeName": "__state", + "operator": "eq", + "attributeValue": "" + } + ] + }, + { + "condition": "AND", + "criterion": [ + { + "attributeName": "__modificationTimestamp", + "operator": "eq", + "attributeValue": "" + }, + { + "attributeName": "__modifiedBy", + "operator": "eq", + "attributeValue": "" + } + ] + } + ] + }, + "attributes": [], + "classification": "_ALL_CLASSIFICATION_TYPES" + } + }, + { + "testDescription": "Nested entity and tag filters with empty values for negative testing", + "searchParameters": { + "typeName": "hive_table", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": { + "condition": "AND", + "criterion": [ + { + "attributeName": "name", + "operator": "eq", + "attributeValue": "" + }, + { + "condition": "AND", + "criterion": [ + { + "attributeName": "owner", + "operator": "eq", + "attributeValue": "" + }, + { + "attributeName": "retention", + "operator": "neq", + "attributeValue": "" + } + ] + } + ] + }, + "tagFilters": { + "condition": "OR", + "criterion": [ + { + "condition": "AND", + "criterion": [ + { + "attributeName": "__createdBy", + "operator": "eq", + "attributeValue": "" + }, + { + "attributeName": "__modificationTimestamp", + "operator": "eq", + "attributeValue": "" + } + ] + }, + { + "attributeName": "__state", + "operator": "eq", + "attributeValue": "" + } + ] + }, + "attributes": [], + "classification": "_ALL_CLASSIFICATION_TYPES" + } + } ] diff --git a/webapp/src/test/resources/json/search-parameters/operator.json b/webapp/src/test/resources/json/search-parameters/operator.json index 224657448..e2939753b 100644 --- a/webapp/src/test/resources/json/search-parameters/operator.json +++ b/webapp/src/test/resources/json/search-parameters/operator.json @@ -1,39 +1,6 @@ -[ { - "testDescription": "hive_table contains testtable or retentionSize != 0", - "searchParameters": { - "typeName": "hive_table", - "excludeDeletedEntities": true, - "classification": "", - "query": "", - "limit": 25, - "offset": 0, - "entityFilters": { - "attributeName": "name", - "attributeValue": "testtable", - "condition" : "AND", - "criterion" : [ - { - "attributeName": "name", - "operator": "invalid_contains", - "attributeValue": "testtable" - }, - { - "attributeName": "retention", - "operator": "neq", - "attributeValue": "0" - } - ] - }, - "tagFilters": null, - "attributes": [ - "" - ] - }, - "expectedCount": 0 -}, - +[ { - "testDescription": "hive_table contains testtable or retentionSize != 0", + "testDescription": "Invalid operator in nested filter (contains → invalid_contains)", "searchParameters": { "typeName": "hive_table", "excludeDeletedEntities": true, @@ -42,32 +9,27 @@ "limit": 25, "offset": 0, "entityFilters": { - "attributeName": "name", - "attributeValue": "testtable", - "condition" : "OR", - "criterion" : [ + "condition": "AND", + "criterion": [ { "attributeName": "name", - "operator": "invalid_eq", + "operator": "eqqq", "attributeValue": "testtable" }, { "attributeName": "retention", - "operator": "invalid_eq", + "operator": "nneq", "attributeValue": "0" } ] }, "tagFilters": null, - "attributes": [ - "" - ] + "attributes": [] }, "expectedCount": 0 }, - { - "testDescription": "hive_table contains testtable or retentionSize != 0", + "testDescription": "Multiple invalid operators in complex nested filter", "searchParameters": { "typeName": "hive_table", "excludeDeletedEntities": true, @@ -76,27 +38,143 @@ "limit": 25, "offset": 0, "entityFilters": { - "attributeName": "name", - "attributeValue": "testtable", - "condition" : "OR", - "criterion" : [ + "condition": "AND", + "criterion": [ { - "attributeName": "name", - "operator": "invalid_neq", - "attributeValue": "testtable" + "condition": "OR", + "criterion": [ + { + "attributeName": "owner", + "operator": "eqqq", + "attributeValue": "hive" + } + ] }, { - "attributeName": "retention", - "operator": "invalid_contains", - "attributeValue": "0" + "condition": "OR", + "criterion": [ + { + "attributeName": "qualifiedName", + "operator": "qqeq", + "attributeValue": "test_db" + }, + { + "attributeName": "qualifiedName", + "operator": "nneq", + "attributeValue": "default" + } + ] + }, + { + "condition": "AND", + "criterion": [ + { + "attributeName": "createTime", + "operator": "neqq", + "attributeValue": "1746057600000,1747785599000" + } + ] } ] }, "tagFilters": null, - "attributes": [ - "" - ] + "attributes": [] }, "expectedCount": 0 + }, + + { + "testDescription": "Only nested tagFilters with invalid operators (eqqq, nneq)", + "searchParameters": { + "typeName": "hive_column", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": null, + "tagFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "OR", + "criterion": [ + { + "attributeName": "name", + "operator": "eqqq", + "attributeValue": "value1" + }, + { + "attributeName": "tagAttr2", + "operator": "nneq", + "attributeValue": "value2" + } + ] + }, + { + "attributeName": "tagAttr3", + "operator": "neqeq", + "attributeValue": "value3" + } + ] + }, + "attributes": [], + "classification": "_ALL_CLASSIFICATION_TYPES" + } + } +, + { + "testDescription": "Nested entityFilters and tagFilters with invalid operators", + "searchParameters": { + "typeName": "hive_table", + "excludeDeletedEntities": true, + "limit": 25, + "offset": 0, + "entityFilters": { + "condition": "AND", + "criterion": [ + { + "condition": "AND", + "criterion": [ + { + "attributeName": "name", + "operator": "eqqq", + "attributeValue": "testtable" + }, + { + "attributeName": "owner", + "operator": "qeq", + "attributeValue": "admin" + } + ] + }, + { + "attributeName": "createTime", + "operator": "ranger", + "attributeValue": "1000,2000" + } + ] + }, + "tagFilters": { + "condition": "OR", + "criterion": [ + { + "condition": "AND", + "criterion": [ + { + "attributeName": "tagAttr1", + "operator": "eqqq", + "attributeValue": "value1" + }, + { + "attributeName": "tagAttr2", + "operator": "isnotnull", + "attributeValue": "" + } + ] + } + ] + }, + "attributes": [], + "classification": "_ALL_CLASSIFICATION_TYPES" + } } ] diff --git a/webapp/src/test/resources/json/search-parameters/tag-filters.json b/webapp/src/test/resources/json/search-parameters/tag-filters.json index 5e74328d6..01ea43553 100644 --- a/webapp/src/test/resources/json/search-parameters/tag-filters.json +++ b/webapp/src/test/resources/json/search-parameters/tag-filters.json @@ -12,5 +12,19 @@ "classification":"fooTag" }, "expectedCount": 1 + }, + { + "testDescription": "Search for exact Tag name with entityFilter and tagFilter with {}", + "searchParameters": { + "entityFilters": {}, + "tagFilters": {}, + "attributes": null, + "query":null, + "excludeDeletedEntities":true, + "limit":25, + "typeName":null, + "classification":"fooTag" + }, + "expectedCount": 1 } ] \ No newline at end of file