This is an automated email from the ASF dual-hosted git repository.
mchades pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new a853a14544 [#12594] fix(server): Check semantic source capabilities
before lookup (#13763)
a853a14544 is described below
commit a853a1454449f79f1953a4d98b90b2fd021f64c6
Author: Akshay Thorat <[email protected]>
AuthorDate: Mon Oct 12 02:36:08 2026 -0700
[#12594] fix(server): Check semantic source capabilities before lookup
(#13763)
### What changes were proposed in this pull request?
Follow up on the post-merge review of #13512:
- Detect Table and View support in one catalog lookup and skip
unsupported source operations, while preserving authorization checks and
errors from supported connectors.
- Load the target Semantic Model before validating replacement sources,
so a missing model returns 404 without looking up its sources.
- Add HTTP and validator regressions, and remove a stale import test
assertion that prevents current main from compiling.
### Why are the changes needed?
A source in a catalog supporting neither Tables nor Views currently
returns 501 rather than 400. Replacing the definition of a nonexistent
model can return a source error (400 or 502) instead of 404.
Related to #12594 and
https://github.com/apache/gravitino/pull/13512#pullrequestreview-5486111328.
Fix: #12594
### Does this PR introduce _any_ user-facing change?
The affected requests return the appropriate 400 or 404 response. No
public API or configuration changes.
### How was this patch tested?
Five HTTP regression cases failed before the fix: three expected 400 but
received 501, and two expected 404 but received 400/502. They pass after
the fix. Additional coverage checks restricted readers, all Table/View
capability combinations, view-only resolution, and propagation of
supported connector failures.
`JAVA_HOME=$(/usr/libexec/java_home -v 17) ./gradlew :server:check
:server:javadoc -PskipITs`
All 652 server tests passed, as did formatting, server checks, and
Javadoc generation. Docker integration tests were not run.
---
.../server/web/rest/SemanticModelOperations.java | 5 +
.../web/rest/SemanticModelSourceValidator.java | 57 ++++++----
.../web/rest/TestSemanticModelOperations.java | 117 +++++++++++++++++++--
.../web/rest/TestSemanticModelSourceValidator.java | 68 +++++++++---
4 files changed, 203 insertions(+), 44 deletions(-)
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
index d75cb5dc91..5e2227bf63 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelOperations.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.server.web.rest;
import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Locale;
import javax.inject.Inject;
@@ -354,6 +355,10 @@ public class SemanticModelOperations {
request.getUpdates().stream()
.map(SemanticModelUpdateRequest::semanticModelChange)
.toArray(SemanticModelChange[]::new);
+ if (Arrays.stream(changes)
+ .anyMatch(change -> change instanceof
SemanticModelChange.ReplaceDefinition)) {
+ dispatcher.loadSemanticModel(ident);
+ }
for (SemanticModelChange change : changes) {
if (change instanceof SemanticModelChange.ReplaceDefinition) {
sourceValidator.validate(
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
index 6b26e7e50a..b77eab345c 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/SemanticModelSourceValidator.java
@@ -19,11 +19,12 @@
package org.apache.gravitino.server.web.rest;
import java.util.Arrays;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
-import java.util.function.Predicate;
+import java.util.function.Function;
import java.util.function.Supplier;
import javax.inject.Inject;
import org.apache.gravitino.Entity;
@@ -33,10 +34,12 @@ import
org.apache.gravitino.authorization.AuthorizationRequestContext;
import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.catalog.TableDispatcher;
import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.connector.CatalogOperations;
import org.apache.gravitino.exceptions.ForbiddenException;
import org.apache.gravitino.exceptions.IllegalSemanticModelException;
import org.apache.gravitino.exceptions.NotFoundException;
import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.TableCatalog;
import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.semantic.Dataset;
import org.apache.gravitino.semantic.Relationship;
@@ -51,7 +54,7 @@ public class SemanticModelSourceValidator {
private final TableDispatcher tables;
private final ViewDispatcher views;
private final Supplier<GravitinoAuthorizer> authorizer;
- private final Predicate<NameIdentifier> supportsViews;
+ private final Function<NameIdentifier, Set<Entity.EntityType>>
supportedSourceTypes;
/**
* Creates a validator using the server's authorizer, including its
disabled-mode pass-through.
@@ -75,15 +78,25 @@ public class SemanticModelSourceValidator {
.catalogManager()
.doWithCatalog(
NameIdentifierUtil.getCatalogIdentifier(ident),
- catalog -> catalog.ops() instanceof ViewCatalog));
+ catalog -> {
+ CatalogOperations ops = catalog.ops();
+ Set<Entity.EntityType> types =
EnumSet.noneOf(Entity.EntityType.class);
+ if (ops instanceof TableCatalog) {
+ types.add(Entity.EntityType.TABLE);
+ }
+ if (ops instanceof ViewCatalog) {
+ types.add(Entity.EntityType.VIEW);
+ }
+ return types;
+ }));
}
SemanticModelSourceValidator(
TableDispatcher tables,
ViewDispatcher views,
Supplier<GravitinoAuthorizer> authorizer,
- Predicate<NameIdentifier> supportsViews) {
- this.supportsViews = supportsViews;
+ Function<NameIdentifier, Set<Entity.EntityType>> supportedSourceTypes) {
+ this.supportedSourceTypes = supportedSourceTypes;
this.tables = tables;
this.views = views;
this.authorizer = authorizer;
@@ -144,13 +157,6 @@ public class SemanticModelSourceValidator {
AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION,
currentAuthorizer,
context);
- if (tableAllowed) {
- try {
- return columnNames(ident, tables.loadTable(ident).columns());
- } catch (NotFoundException missing) {
- // The untyped source can also refer to a logical view. Authorize that
lookup separately.
- }
- }
boolean viewAllowed =
canLoad(
ident,
@@ -158,18 +164,27 @@ public class SemanticModelSourceValidator {
AuthorizationExpressionConstants.LOAD_VIEW_AUTHORIZATION_EXPRESSION,
currentAuthorizer,
context);
- boolean viewSupported = true;
- if (tableAllowed || viewAllowed) {
- try {
- viewSupported = supportsViews.test(ident);
- if (viewAllowed && viewSupported) {
- return columnNames(ident, views.loadView(ident).columns());
+ if (!tableAllowed && !viewAllowed) {
+ throw new ForbiddenException("Not authorized to resolve Semantic Model
source %s", ident);
+ }
+ Set<Entity.EntityType> supported = EnumSet.of(Entity.EntityType.TABLE,
Entity.EntityType.VIEW);
+ try {
+ supported = supportedSourceTypes.apply(ident);
+ if (tableAllowed && supported.contains(Entity.EntityType.TABLE)) {
+ try {
+ return columnNames(ident, tables.loadTable(ident).columns());
+ } catch (NotFoundException missing) {
+ // An untyped source can also refer to a view.
}
- } catch (NotFoundException missing) {
- // Do not disclose whether the other, inaccessible object type exists.
}
+ if (viewAllowed && supported.contains(Entity.EntityType.VIEW)) {
+ return columnNames(ident, views.loadView(ident).columns());
+ }
+ } catch (NotFoundException missing) {
+ // Do not disclose whether the other, inaccessible object type exists.
}
- if (!tableAllowed || (viewSupported && !viewAllowed)) {
+ if ((supported.contains(Entity.EntityType.TABLE) && !tableAllowed)
+ || (supported.contains(Entity.EntityType.VIEW) && !viewAllowed)) {
throw new ForbiddenException("Not authorized to resolve Semantic Model
source %s", ident);
}
throw new IllegalSemanticModelException("Semantic Model source %s does not
exist", ident);
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
index c5d7f6b098..0856bd1879 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelOperations.java
@@ -26,6 +26,7 @@ import static org.mockito.Mockito.doCallRealMethod;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -41,6 +42,7 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
+import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.stream.IntStream;
@@ -135,6 +137,7 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
private final SemanticModelDispatcher dispatcher =
mock(SemanticModelDispatcher.class);
private final TableDispatcher tables = mock(TableDispatcher.class);
private final ViewDispatcher views = mock(ViewDispatcher.class);
+ private boolean tablesSupported = true;
private boolean viewsSupported = true;
private GravitinoAuthorizer sourceAuthorizer = new PassThroughAuthorizer();
private final String metalake = "semantic_model_metalake";
@@ -160,7 +163,15 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
protected void configure() {
bind(dispatcher).to(SemanticModelDispatcher.class).ranked(2);
bind(new SemanticModelSourceValidator(
- tables, views, () -> sourceAuthorizer, ident ->
viewsSupported))
+ tables,
+ views,
+ () -> sourceAuthorizer,
+ ident -> {
+ EnumSet<EntityType> types =
EnumSet.noneOf(EntityType.class);
+ if (tablesSupported) types.add(EntityType.TABLE);
+ if (viewsSupported) types.add(EntityType.VIEW);
+ return types;
+ }))
.to(SemanticModelSourceValidator.class);
bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class);
}
@@ -171,6 +182,7 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
@BeforeEach
void resetDispatcher() {
reset(dispatcher, tables, views);
+ tablesSupported = true;
viewsSupported = true;
sourceAuthorizer = new PassThroughAuthorizer();
Table table = mock(Table.class);
@@ -178,6 +190,7 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
when(column.name()).thenReturn("order_id");
when(table.columns()).thenReturn(new Column[] {column});
when(tables.loadTable(any())).thenReturn(table);
+
when(dispatcher.loadSemanticModel(any())).thenReturn(semanticModel("sales",
null));
doCallRealMethod().when(dispatcher).exportOssieDocument(any(), any());
}
@@ -215,7 +228,11 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
SemanticModelOperations operations =
new SemanticModelOperations(
dispatcher,
- new SemanticModelSourceValidator(tables, views, () ->
sourceAuthorizer, ident -> true));
+ new SemanticModelSourceValidator(
+ tables,
+ views,
+ () -> sourceAuthorizer,
+ ident -> EnumSet.of(EntityType.TABLE, EntityType.VIEW)));
FieldUtils.writeField(operations, "httpRequest",
mock(HttpServletRequest.class), true);
try (MockedStatic<MetadataAuthzHelper> helper =
mockStatic(MetadataAuthzHelper.class)) {
helper
@@ -286,7 +303,8 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
code,
type.getSimpleName(),
message);
- verifyNoInteractions(dispatcher, views);
+ verifyNoModelWrites();
+ verifyNoInteractions(views);
if (failure.equals("denied")) {
verifyNoInteractions(tables);
}
@@ -682,7 +700,7 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
IllegalSemanticModelException.class.getSimpleName(),
"does not exist");
- verifyNoInteractions(dispatcher);
+ verifyNoModelWrites();
}
@ParameterizedTest
@@ -703,7 +721,8 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
IllegalSemanticModelException.class.getSimpleName(),
"does not exist");
- verifyNoInteractions(dispatcher, views);
+ verifyNoModelWrites();
+ verifyNoInteractions(views);
}
@ParameterizedTest
@@ -758,7 +777,75 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
"does not exist");
}
verify(tables).loadTable(NameIdentifier.of(metalake, catalog, schema,
"orders"));
- verifyNoInteractions(views, dispatcher);
+ verifyNoModelWrites();
+ verifyNoInteractions(views);
+ }
+
+ @ParameterizedTest
+ @CsvSource({
+ "create,false",
+ "replace,false",
+ "import,false",
+ "create,true",
+ "replace,true",
+ "import,true"
+ })
+ void testSourceInCatalogWithoutTablesOrViews(String operation, boolean
tableOnlyReader) {
+ if (tableOnlyReader) {
+ sourceAuthorizer = mock(GravitinoAuthorizer.class);
+ when(sourceAuthorizer.authorize(any(), any(), any(), any(), any()))
+ .thenAnswer(
+ invocation -> {
+ Privilege.Name privilege = invocation.getArgument(3);
+ return privilege == Privilege.Name.USE_CATALOG
+ || privilege == Privilege.Name.USE_SCHEMA
+ || privilege == Privilege.Name.SELECT_TABLE;
+ });
+ }
+ tablesSupported = false;
+ viewsSupported = false;
+ when(tables.loadTable(any()))
+ .thenThrow(new UnsupportedOperationException("Catalog does not support
table operations"));
+ Response response =
+ operation.equals("import")
+ ? postDocument(
+ semanticModelPath() + "/ossie",
+ "version: 0.2.0.dev0\nname: sales\ndatasets:\n - name:
orders\n source: "
+ + catalog
+ + "."
+ + schema
+ + ".orders\n",
+ "application/yaml")
+ : writeDefinition(operation.equals("replace"),
semanticModelDefinition());
+ assertError(
+ response,
+ Response.Status.BAD_REQUEST,
+ ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
+ IllegalSemanticModelException.class.getSimpleName(),
+ "does not exist");
+ verifyNoInteractions(tables, views);
+ verifyNoModelWrites();
+ }
+
+ @ParameterizedTest
+ @ValueSource(booleans = {false, true})
+ void testMissingModelBeforeSourceValidation(boolean unavailableSource) {
+ when(dispatcher.loadSemanticModel(semanticModelIdentifier("sales")))
+ .thenThrow(new NoSuchSemanticModelException("sales does not exist"));
+ when(tables.loadTable(any()))
+ .thenThrow(
+ unavailableSource
+ ? new ConnectionFailedException("offline")
+ : new NoSuchTableException("missing"));
+ when(views.loadView(any())).thenThrow(new NoSuchViewException("missing"));
+ assertError(
+ writeDefinition(true, semanticModelDefinition()),
+ Response.Status.NOT_FOUND,
+ ErrorConstants.NOT_FOUND_CODE,
+ NoSuchSemanticModelException.class.getSimpleName(),
+ "sales does not exist");
+ verifyNoInteractions(tables, views);
+ verifyNoModelWrites();
}
@ParameterizedTest
@@ -773,7 +860,7 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
IllegalSemanticModelException.class.getSimpleName(),
"order_id");
- verifyNoInteractions(dispatcher);
+ verifyNoModelWrites();
}
@ParameterizedTest
@@ -786,7 +873,8 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
ErrorConstants.FORBIDDEN_CODE,
ForbiddenException.class.getSimpleName(),
"Not authorized");
- verifyNoInteractions(tables, views, dispatcher);
+ verifyNoModelWrites();
+ verifyNoInteractions(tables, views);
}
@ParameterizedTest
@@ -799,7 +887,8 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
ErrorConstants.CONNECTION_FAILED_CODE,
ConnectionFailedException.class.getSimpleName(),
"source unavailable");
- verifyNoInteractions(views, dispatcher);
+ verifyNoModelWrites();
+ verifyNoInteractions(views);
}
@ParameterizedTest
@@ -841,7 +930,8 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
ErrorConstants.ILLEGAL_ARGUMENTS_CODE,
IllegalArgumentException.class.getSimpleName(),
"Metadata object full name cannot be blank");
- verifyNoInteractions(tables, views, dispatcher);
+ verifyNoModelWrites();
+ verifyNoInteractions(tables, views);
return;
}
if (sourceExists) {
@@ -861,7 +951,7 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
IllegalSemanticModelException.class.getSimpleName(),
"does not exist");
verify(views).loadView(fullSource);
- verifyNoInteractions(dispatcher);
+ verifyNoModelWrites();
}
verify(tables).loadTable(fullSource);
}
@@ -884,6 +974,11 @@ public class TestSemanticModelOperations extends
BaseOperationsTest {
segment, replace,
authorized, exists)))));
}
+ private void verifyNoModelWrites() {
+ verify(dispatcher, never()).createSemanticModel(any(), any(), any(),
any());
+ verify(dispatcher, never()).alterSemanticModel(any(),
any(SemanticModelChange[].class));
+ }
+
private Response writeDefinition(boolean replaceDefinition,
SemanticModelDefinition definition) {
return replaceDefinition
? put(
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
index aa7544f830..3395c8827b 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestSemanticModelSourceValidator.java
@@ -32,7 +32,10 @@ import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
-import java.util.function.Predicate;
+import java.util.EnumSet;
+import java.util.Set;
+import java.util.function.Function;
+import org.apache.gravitino.Entity.EntityType;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
@@ -50,6 +53,7 @@ import org.apache.gravitino.exceptions.NoSuchTableException;
import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.rel.Column;
import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.TableCatalog;
import org.apache.gravitino.rel.View;
import org.apache.gravitino.rel.ViewCatalog;
import org.apache.gravitino.semantic.Dataset;
@@ -59,7 +63,7 @@ import
org.apache.gravitino.server.authorization.PassThroughAuthorizer;
import org.apache.gravitino.utils.ThrowableFunction;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
+import org.junit.jupiter.params.provider.CsvSource;
import org.mockito.MockedStatic;
/** Tests catalog-backed validation independently of model persistence. */
@@ -73,7 +77,8 @@ public class TestSemanticModelSourceValidator {
private final ViewDispatcher views = mock(ViewDispatcher.class);
private GravitinoAuthorizer authorizer = new PassThroughAuthorizer();
private final SemanticModelSourceValidator validator =
- new SemanticModelSourceValidator(tables, views, () -> authorizer, ident
-> true);
+ new SemanticModelSourceValidator(
+ tables, views, () -> authorizer, ident ->
EnumSet.of(EntityType.TABLE, EntityType.VIEW));
@Test
void testCrossCatalogSourceAndKeysWithDisabledAuthorization() {
@@ -201,19 +206,25 @@ public class TestSemanticModelSourceValidator {
}
@ParameterizedTest
- @ValueSource(booleans = {false, true})
- void testViewSupportCheckedBeforeFallback(boolean supportsViews) {
+ @CsvSource({"false,false", "false,true", "true,false", "true,true"})
+ void testSourceSupportCheckedBeforeLookup(boolean supportsTables, boolean
supportsViews) {
CatalogManager catalogs = mock(CatalogManager.class);
BaseCatalog catalog = mock(BaseCatalog.class);
+ Class<?>[] interfaces =
+ supportsTables
+ ? (supportsViews
+ ? new Class<?>[] {TableCatalog.class, ViewCatalog.class}
+ : new Class<?>[] {TableCatalog.class})
+ : (supportsViews ? new Class<?>[] {ViewCatalog.class} : new
Class<?>[0]);
CatalogOperations operations =
- supportsViews
- ? mock(CatalogOperations.class,
withSettings().extraInterfaces(ViewCatalog.class))
- : mock(CatalogOperations.class);
+ interfaces.length == 0
+ ? mock(CatalogOperations.class)
+ : mock(CatalogOperations.class,
withSettings().extraInterfaces(interfaces));
when(catalog.ops()).thenReturn(operations);
when(catalogs.doWithCatalog(eq(NameIdentifier.of("metalake",
"other_catalog")), any()))
.thenAnswer(
invocation -> {
- ThrowableFunction<BaseCatalog, Boolean> callback =
invocation.getArgument(1);
+ ThrowableFunction<BaseCatalog, Set<EntityType>> callback =
invocation.getArgument(1);
return callback.apply(catalog);
});
when(tables.loadTable(FULL_SOURCE)).thenThrow(new
NoSuchTableException("missing"));
@@ -227,6 +238,11 @@ public class TestSemanticModelSourceValidator {
assertThrows(
IllegalSemanticModelException.class,
() -> checking.validate("metalake", definition(dataset("orders",
null, null))));
+ if (supportsTables) {
+ verify(tables).loadTable(FULL_SOURCE);
+ } else {
+ verifyNoInteractions(tables);
+ }
if (supportsViews) {
verify(views).loadView(FULL_SOURCE);
} else {
@@ -236,6 +252,34 @@ public class TestSemanticModelSourceValidator {
}
}
+ @Test
+ void testViewOnlyCatalogCanResolveSourceWithoutTableLookup() {
+ authorizeOnly(MetadataObject.Type.VIEW, Privilege.Name.SELECT_VIEW);
+ View view = mock(View.class);
+ doReturn(columns("id")).when(view).columns();
+ when(views.loadView(FULL_SOURCE)).thenReturn(view);
+ SemanticModelSourceValidator checking =
+ new SemanticModelSourceValidator(
+ tables, views, () -> authorizer, ident ->
EnumSet.of(EntityType.VIEW));
+ checking.validate("metalake", definition(dataset("orders", new String[]
{"id"}, null)));
+ verifyNoInteractions(tables);
+ verify(views).loadView(FULL_SOURCE);
+ when(views.loadView(FULL_SOURCE)).thenThrow(new
NoSuchViewException("missing"));
+ assertThrows(
+ IllegalSemanticModelException.class,
+ () -> checking.validate("metalake", definition(dataset("orders", null,
null))));
+ }
+
+ @Test
+ void testSupportedTableOperationFailureIsNotHidden() {
+ when(tables.loadTable(FULL_SOURCE))
+ .thenThrow(new UnsupportedOperationException("connector failure"));
+ assertThrows(
+ UnsupportedOperationException.class,
+ () -> validator.validate("metalake", definition(dataset("orders",
null, null))));
+ verifyNoInteractions(views);
+ }
+
@Test
void testSupportedViewOperationFailureIsNotHidden() {
when(tables.loadTable(FULL_SOURCE)).thenThrow(new
NoSuchTableException("missing"));
@@ -249,13 +293,13 @@ public class TestSemanticModelSourceValidator {
@Test
void testDeniedSourcesDoNotProbeCatalogCapabilities() {
authorizer = mock(GravitinoAuthorizer.class);
- Predicate<NameIdentifier> supportsViews = mock(Predicate.class);
+ Function<NameIdentifier, Set<EntityType>> supportedSourceTypes =
mock(Function.class);
SemanticModelSourceValidator checking =
- new SemanticModelSourceValidator(tables, views, () -> authorizer,
supportsViews);
+ new SemanticModelSourceValidator(tables, views, () -> authorizer,
supportedSourceTypes);
assertThrows(
ForbiddenException.class,
() -> checking.validate("metalake", definition(dataset("orders", null,
null))));
- verifyNoInteractions(tables, views, supportsViews);
+ verifyNoInteractions(tables, views, supportedSourceTypes);
}
private void authorizeOnly(MetadataObject.Type type, Privilege.Name
privilege) {