This is an automated email from the ASF dual-hosted git repository.
jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new ef639b3b38 [#13845] fix(server): Backport authorized view existence
probing to branch-1.3 (#14191)
ef639b3b38 is described below
commit ef639b3b38d40675f48caf4830076455efd0d4e5
Author: jarred0214 <[email protected]>
AuthorDate: Mon Oct 26 14:57:36 2026 +0800
[#13845] fix(server): Backport authorized view existence probing to
branch-1.3 (#14191)
### What changes were proposed in this pull request?
Backport #13846 (commit 9f8197917b6918e7f62ae7e2eac4c825d537de60) to
`branch-1.3`, resolving the conflicts in #14074.
Add authorized existence probing for view loads and use internal
dispatchers for table/view probes. Return a terminal 404 for absent
objects, preventing concurrent creation from exposing metadata. Adapt
the branch-specific imports and test mocks.
### Why are the changes needed?
Clients need to distinguish absent views from inaccessible existing
views when they have permission to probe existence. Probe results must
not allow a subsequent unauthorized load.
Refs #13845. Supersedes #14074.
### Does this PR introduce _any_ user-facing change?
Authorized probes of absent views return 404; existing views remain
forbidden without load permission. No API or configuration changes.
### How was this patch tested?
- Passed 33 tests: `./gradlew :server:spotlessApply :server:test
-PskipITs --tests '*TestLoadTableAuthorizationExecutor' --tests
'*TestLoadViewAuthorizationExecutor' --tests
'*TestGravitinoInterceptionService'`.
- `git diff --check` passed.
- Full `./gradlew spotlessApply` was attempted; the unrelated
`:mcp-server:formatApplyPython` task failed because its installed Black
extension is x86_64 on an ARM host. The affected Java formatting/checks
passed.
---
.../annotations/AuthorizationRequest.java | 2 +
.../web/filter/GravitinoInterceptionService.java | 26 ++
.../authorization/AuthorizeExecutorFactory.java | 2 +
.../LoadTableAuthorizationExecutor.java | 6 +-
.../LoadViewAuthorizationExecutor.java | 79 ++++++
.../gravitino/server/web/rest/ViewOperations.java | 8 +-
.../filter/TestGravitinoInterceptionService.java | 297 ++++++++++++++++++++-
.../TestLoadTableAuthorizationExecutor.java | 83 ++++++
.../TestLoadViewAuthorizationExecutor.java | 170 ++++++++++++
9 files changed, 658 insertions(+), 15 deletions(-)
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
index f90679324f..8587a2aa99 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationRequest.java
@@ -35,6 +35,8 @@ public @interface AuthorizationRequest {
RUN_JOB,
LINEAGE,
LOAD_TABLE,
+ /** Loads a view with authorized existence probing. */
+ LOAD_VIEW,
CREATE_SCHEMA,
TEST_CATALOG_CONNECTION
}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index a21fd32e01..e5440fdd70 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -48,6 +48,8 @@ import org.apache.gravitino.exceptions.ForbiddenException;
import org.apache.gravitino.exceptions.IllegalMetadataObjectException;
import org.apache.gravitino.exceptions.IllegalNameIdentifierException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.lineage.source.rest.LineageOperations;
import
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
@@ -57,6 +59,7 @@ import org.apache.gravitino.server.web.Utils;
import
org.apache.gravitino.server.web.filter.authorization.AuthorizationExecutor;
import
org.apache.gravitino.server.web.filter.authorization.AuthorizeExecutorFactory;
import org.apache.gravitino.server.web.rest.CatalogOperations;
+import org.apache.gravitino.server.web.rest.ExceptionHandlers;
import org.apache.gravitino.server.web.rest.FilesetOperations;
import org.apache.gravitino.server.web.rest.FunctionOperations;
import org.apache.gravitino.server.web.rest.GroupOperations;
@@ -66,6 +69,7 @@ import
org.apache.gravitino.server.web.rest.MetadataObjectPolicyOperations;
import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
import org.apache.gravitino.server.web.rest.MetalakeOperations;
import org.apache.gravitino.server.web.rest.ModelOperations;
+import org.apache.gravitino.server.web.rest.OperationType;
import org.apache.gravitino.server.web.rest.OwnerOperations;
import org.apache.gravitino.server.web.rest.PartitionOperations;
import org.apache.gravitino.server.web.rest.PermissionOperations;
@@ -78,6 +82,7 @@ import org.apache.gravitino.server.web.rest.TagOperations;
import org.apache.gravitino.server.web.rest.TopicOperations;
import org.apache.gravitino.server.web.rest.UserOperations;
import org.apache.gravitino.server.web.rest.ViewOperations;
+import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.PrincipalUtils;
import org.glassfish.hk2.api.Descriptor;
import org.glassfish.hk2.api.Filter;
@@ -230,6 +235,27 @@ public class GravitinoInterceptionService implements
InterceptionService {
boolean authorizeResult;
try {
authorizeResult = executor.execute(authorizationRequestContext);
+ } catch (NoSuchTableException exception) {
+ NameIdentifier tableIdent =
metadataContext.get(Entity.EntityType.TABLE);
+ if (tableIdent == null) {
+ throw exception;
+ }
+ return ExceptionHandlers.handleTableException(
+ OperationType.LOAD,
+ tableIdent.name(),
+ NameIdentifierUtil.getSchemaIdentifier(tableIdent).name(),
+ exception);
+ } catch (NoSuchViewException exception) {
+ NameIdentifier viewIdent =
metadataContext.get(Entity.EntityType.VIEW);
+ if (viewIdent == null) {
+ throw exception;
+ }
+ // An authorized absence probe must terminate before the
resource can load a view.
+ return ExceptionHandlers.handleViewException(
+ OperationType.LOAD,
+ viewIdent.name(),
+ NameIdentifierUtil.getSchemaIdentifier(viewIdent).name(),
+ exception);
} catch (BadRequestException exception) {
LOG.warn("Invalid authorization request", exception);
return Utils.illegalArguments(exception.getMessage(), exception);
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
index 12a526cb2b..e4f5be3749 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AuthorizeExecutorFactory.java
@@ -58,6 +58,8 @@ public class AuthorizeExecutorFactory {
secondaryExpression,
secondaryExpressionCondition,
allowCheckExistenceExpression);
+ case LOAD_VIEW -> new LoadViewAuthorizationExecutor(
+ expression, metadataContext, pathParams, entityType,
allowCheckExistenceExpression);
case CREATE_SCHEMA -> new CreateSchemaAuthorizationExecutor(
parameters, args, expression, metadataContext, pathParams,
entityType);
case TEST_CATALOG_CONNECTION -> new
CatalogConnectionTestAuthorizationExecutor(
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
index a0a99b94e4..36b49e43e1 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadTableAuthorizationExecutor.java
@@ -87,8 +87,10 @@ public class LoadTableAuthorizationExecutor extends
CommonAuthorizerExecutor {
}
NameIdentifier tableIdentifier =
metadataContext.get(Entity.EntityType.TABLE);
- return tableIdentifier != null
- &&
!GravitinoEnv.getInstance().tableDispatcher().tableExists(tableIdentifier);
+ if (tableIdentifier != null) {
+
GravitinoEnv.getInstance().internalTableDispatcher().loadTable(tableIdentifier);
+ }
+ return false;
}
private static boolean shouldCheckModifyTablePrivilege(
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadViewAuthorizationExecutor.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadViewAuthorizationExecutor.java
new file mode 100644
index 0000000000..5a594f744f
--- /dev/null
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LoadViewAuthorizationExecutor.java
@@ -0,0 +1,79 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.web.filter.authorization;
+
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
+
+/** Authorizes view loads and permits authorized probes of absent views. */
+public class LoadViewAuthorizationExecutor extends CommonAuthorizerExecutor {
+ @Nullable private final AuthorizationExpressionEvaluator
allowCheckExistenceEvaluator;
+
+ /**
+ * Creates an executor for loading views.
+ *
+ * @param expression the primary authorization expression
+ * @param metadataContext the metadata identifiers
+ * @param pathParams the request path parameters
+ * @param entityType the optional entity type
+ * @param allowCheckExistenceExpression the expression permitting existence
probes
+ */
+ public LoadViewAuthorizationExecutor(
+ String expression,
+ Map<Entity.EntityType, NameIdentifier> metadataContext,
+ Map<String, Object> pathParams,
+ Optional<String> entityType,
+ String allowCheckExistenceExpression) {
+ super(expression, metadataContext, pathParams, entityType);
+ allowCheckExistenceEvaluator =
+ StringUtils.isBlank(allowCheckExistenceExpression)
+ ? null
+ : new
AuthorizationExpressionEvaluator(allowCheckExistenceExpression);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public boolean execute(AuthorizationRequestContext
authorizationRequestContext) throws Exception {
+ if (super.execute(authorizationRequestContext)) {
+ return true;
+ }
+
+ if (allowCheckExistenceEvaluator == null
+ || !allowCheckExistenceEvaluator.evaluate(
+ metadataContext, pathParams, authorizationRequestContext,
entityType)) {
+ return false;
+ }
+
+ NameIdentifier viewIdentifier =
metadataContext.get(Entity.EntityType.VIEW);
+ if (viewIdentifier == null) {
+ return false;
+ }
+ try {
+
GravitinoEnv.getInstance().internalViewDispatcher().loadView(viewIdentifier);
+ } catch (UnsupportedOperationException e) {
+ // Preserve the primary denial when the connector cannot perform the
existence probe.
+ }
+ return false;
+ }
+}
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
index 794298b25f..786d41ea29 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/ViewOperations.java
@@ -49,6 +49,7 @@ import org.apache.gravitino.rel.ViewChange;
import org.apache.gravitino.server.authorization.MetadataAuthzHelper;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
import org.apache.gravitino.server.web.Utils;
import org.apache.gravitino.utils.NameIdentifierUtil;
@@ -163,13 +164,18 @@ public class ViewOperations {
@ResponseMetered(name = "load-view", absolute = true)
@AuthorizationExpression(
expression =
AuthorizationExpressionConstants.LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+ allowCheckExistence =
+
AuthorizationExpressionConstants.PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION,
accessMetadataType = MetadataObject.Type.VIEW)
public Response loadView(
@PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
String metalake,
@PathParam("catalog") @AuthorizationMetadata(type =
Entity.EntityType.CATALOG) String catalog,
@PathParam("schema") @AuthorizationMetadata(type =
Entity.EntityType.SCHEMA) String schema,
- @PathParam("view") @AuthorizationMetadata(type = Entity.EntityType.VIEW)
String view) {
+ @PathParam("view")
+ @AuthorizationMetadata(type = Entity.EntityType.VIEW)
+ @AuthorizationRequest(type =
AuthorizationRequest.RequestType.LOAD_VIEW)
+ String view) {
LOG.info("Received load view request for view: {}.{}.{}.{}", metalake,
catalog, schema, view);
try {
return Utils.doAs(
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
index d29ade70ca..33ffa50053 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
@@ -34,8 +34,11 @@ import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.lang.reflect.Method;
import java.security.Principal;
+import java.security.PrivilegedExceptionAction;
+import java.time.Instant;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.core.Response;
import org.aopalliance.intercept.MethodInterceptor;
@@ -61,9 +64,21 @@ import org.apache.gravitino.dto.responses.ErrorConstants;
import org.apache.gravitino.dto.responses.ErrorResponse;
import org.apache.gravitino.exceptions.ForbiddenException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.exceptions.NoSuchViewException;
import org.apache.gravitino.listener.EventBus;
import
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
+import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.metalake.MetalakeManager;
+import org.apache.gravitino.rel.Column;
+import org.apache.gravitino.rel.Representation;
+import org.apache.gravitino.rel.SQLRepresentation;
+import org.apache.gravitino.rel.Table;
+import org.apache.gravitino.rel.View;
+import org.apache.gravitino.rel.expressions.distributions.Distributions;
+import org.apache.gravitino.rel.expressions.sorts.SortOrder;
+import org.apache.gravitino.rel.expressions.transforms.Transform;
+import org.apache.gravitino.rel.indexes.Index;
import org.apache.gravitino.server.authorization.GravitinoAuthorizerProvider;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationFullName;
@@ -633,8 +648,7 @@ public class TestGravitinoInterceptionService {
}
@Test
- public void
testLoadTableAuthorizationProceedsWhenProbeAllowedTableDoesNotExist()
- throws Throwable {
+ void testLoadTableAuthorizationTerminatesWhenProbeAllowedTableDoesNotExist()
throws Throwable {
try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
mockStatic(GravitinoAuthorizerProvider.class);
@@ -661,9 +675,10 @@ public class TestGravitinoInterceptionService {
TableDispatcher tableDispatcher = mock(TableDispatcher.class);
EventBus mockEventBus = mock(EventBus.class);
envMocked.when(GravitinoEnv::getInstance).thenReturn(mockEnv);
- when(mockEnv.tableDispatcher()).thenReturn(tableDispatcher);
+ when(mockEnv.internalTableDispatcher()).thenReturn(tableDispatcher);
when(mockEnv.eventBus()).thenReturn(mockEventBus);
-
when(tableDispatcher.tableExists(ArgumentMatchers.any())).thenReturn(false);
+ when(tableDispatcher.loadTable(ArgumentMatchers.any()))
+ .thenThrow(new NoSuchTableException("Table is absent"));
MethodInterceptor interceptor = tableLoadInterceptor();
MethodInvocation invocation = tableLoadInvocation();
@@ -673,8 +688,11 @@ public class TestGravitinoInterceptionService {
Response response = (Response) interceptor.invoke(invocation);
assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
response.getStatus());
- verify(invocation).proceed();
- verify(tableDispatcher).tableExists(ArgumentMatchers.any());
+ verify(invocation, never()).proceed();
+ assertEquals(
+ NoSuchTableException.class.getSimpleName(),
+ ((ErrorResponse) response.getEntity()).getType());
+ verify(tableDispatcher).loadTable(ArgumentMatchers.any());
verify(mockEventBus, never()).dispatchEvent(ArgumentMatchers.any());
}
}
@@ -708,9 +726,9 @@ public class TestGravitinoInterceptionService {
TableDispatcher tableDispatcher = mock(TableDispatcher.class);
EventBus mockEventBus = spy(new EventBus(Collections.emptyList()));
envMocked.when(GravitinoEnv::getInstance).thenReturn(mockEnv);
- when(mockEnv.tableDispatcher()).thenReturn(tableDispatcher);
+ when(mockEnv.internalTableDispatcher()).thenReturn(tableDispatcher);
when(mockEnv.eventBus()).thenReturn(mockEventBus);
-
when(tableDispatcher.tableExists(ArgumentMatchers.any())).thenReturn(true);
+
when(tableDispatcher.loadTable(ArgumentMatchers.any())).thenReturn(mock(Table.class));
MethodInterceptor interceptor = tableLoadInterceptor();
MethodInvocation invocation = tableLoadInvocation();
@@ -719,12 +737,264 @@ public class TestGravitinoInterceptionService {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(),
response.getStatus());
verify(invocation, never()).proceed();
- verify(tableDispatcher).tableExists(ArgumentMatchers.any());
+ verify(tableDispatcher).loadTable(ArgumentMatchers.any());
+ verify(mockEventBus)
+
.dispatchEvent(ArgumentMatchers.any(AuthorizationDenialFailureEvent.class));
+ }
+ }
+
+ /**
+ * Unsupported view probes preserve the primary denial and its authorization
event.
+ *
+ * @throws Throwable If authorization interception fails.
+ */
+ @Test
+ public void
testLoadViewAuthorizationReturnsForbiddenWhenViewsAreUnsupported() throws
Throwable {
+ try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
+ MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+ mockStatic(GravitinoAuthorizerProvider.class);
+ MockedStatic<AuthorizationUtils> authUtilsMocked =
mockStatic(AuthorizationUtils.class);
+ MockedStatic<GravitinoEnv> envMocked = mockStatic(GravitinoEnv.class))
{
+ principalUtilsMocked
+ .when(PrincipalUtils::getCurrentPrincipal)
+ .thenReturn(new UserPrincipal("tester"));
+
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+
+ authUtilsMocked
+ .when(
+ () ->
+ AuthorizationUtils.checkCurrentUser(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation -> null);
+
+ GravitinoAuthorizerProvider mockedProvider =
mock(GravitinoAuthorizerProvider.class);
+ GravitinoAuthorizer authorizer = tableProbeAuthorizer();
+
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
+ when(mockedProvider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+ GravitinoEnv mockEnv = mock(GravitinoEnv.class);
+ ViewDispatcher viewDispatcher = mock(ViewDispatcher.class);
+ EventBus mockEventBus = spy(new EventBus(Collections.emptyList()));
+ envMocked.when(GravitinoEnv::getInstance).thenReturn(mockEnv);
+ when(mockEnv.internalViewDispatcher()).thenReturn(viewDispatcher);
+ when(mockEnv.eventBus()).thenReturn(mockEventBus);
+ when(viewDispatcher.loadView(ArgumentMatchers.any()))
+ .thenThrow(new UnsupportedOperationException("Catalog does not
support view operations"));
+
+ Method method =
+ ViewOperations.class.getMethod(
+ "loadView", String.class, String.class, String.class,
String.class);
+ MethodInterceptor interceptor =
+ new
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+ MethodInvocation invocation = mock(MethodInvocation.class);
+ when(invocation.getMethod()).thenReturn(method);
+ when(invocation.getArguments())
+ .thenReturn(new Object[] {"testMetalake", "testCatalog",
"testSchema", "missingView"});
+
+ Response response = (Response) interceptor.invoke(invocation);
+
+ assertEquals(Response.Status.FORBIDDEN.getStatusCode(),
response.getStatus());
+ verify(invocation, never()).proceed();
+ verify(viewDispatcher).loadView(ArgumentMatchers.any());
verify(mockEventBus)
.dispatchEvent(ArgumentMatchers.any(AuthorizationDenialFailureEvent.class));
}
}
+ /**
+ * An absent view probe is terminal even when a concurrent create makes the
view loadable.
+ *
+ * @throws Throwable If authorization interception fails.
+ */
+ @Test
+ public void testMissingViewProbeDoesNotLoadConcurrentlyCreatedView() throws
Throwable {
+ try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
+ MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+ mockStatic(GravitinoAuthorizerProvider.class);
+ MockedStatic<AuthorizationUtils> authUtilsMocked =
mockStatic(AuthorizationUtils.class);
+ MockedStatic<GravitinoEnv> envMocked = mockStatic(GravitinoEnv.class))
{
+ principalUtilsMocked
+ .when(PrincipalUtils::getCurrentPrincipal)
+ .thenReturn(new UserPrincipal("tester"));
+
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+
+ authUtilsMocked
+ .when(
+ () ->
+ AuthorizationUtils.checkCurrentUser(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation -> null);
+
+ GravitinoAuthorizerProvider mockedProvider =
mock(GravitinoAuthorizerProvider.class);
+ GravitinoAuthorizer authorizer = tableProbeAuthorizer();
+
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
+ when(mockedProvider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+ GravitinoEnv mockEnv = mock(GravitinoEnv.class);
+ ViewDispatcher viewDispatcher = mock(ViewDispatcher.class);
+ EventBus mockEventBus = spy(new EventBus(Collections.emptyList()));
+ envMocked.when(GravitinoEnv::getInstance).thenReturn(mockEnv);
+ when(mockEnv.internalViewDispatcher()).thenReturn(viewDispatcher);
+ when(mockEnv.eventBus()).thenReturn(mockEventBus);
+ AtomicReference<View> created = new AtomicReference<>();
+ View view = mock(View.class);
+ when(view.name()).thenReturn("missingView");
+ when(view.columns()).thenReturn(new Column[0]);
+ when(view.representations())
+ .thenReturn(
+ new Representation[] {
+ SQLRepresentation.builder()
+ .withDialect("spark")
+ .withSql("SELECT secret FROM private_table")
+ .build()
+ });
+ when(view.properties()).thenReturn(Collections.emptyMap());
+ when(view.auditInfo())
+ .thenReturn(
+
AuditInfo.builder().withCreator("owner").withCreateTime(Instant.now()).build());
+ when(viewDispatcher.loadView(ArgumentMatchers.any()))
+ .thenAnswer(
+ ignored -> {
+ if (created.get() == null) {
+ // The load observed absence; another request creates the
view before the handler
+ // can run.
+ created.set(view);
+ throw new NoSuchViewException("View is absent");
+ }
+ return created.get();
+ });
+ ViewOperations operations = new ViewOperations(viewDispatcher);
+ FieldUtils.writeField(operations, "httpRequest",
mock(HttpServletRequest.class), true);
+ principalUtilsMocked
+ .when(() -> PrincipalUtils.doAs(any(), any()))
+ .thenAnswer(
+ invocation -> ((PrivilegedExceptionAction<?>)
invocation.getArgument(1)).run());
+
+ Method method =
+ ViewOperations.class.getMethod(
+ "loadView", String.class, String.class, String.class,
String.class);
+ MethodInterceptor interceptor =
+ new
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+ MethodInvocation invocation = mock(MethodInvocation.class);
+ when(invocation.getMethod()).thenReturn(method);
+ when(invocation.getArguments())
+ .thenReturn(new Object[] {"testMetalake", "testCatalog",
"testSchema", "missingView"});
+
+ when(invocation.proceed())
+ .thenAnswer(
+ ignored ->
+ operations.loadView("testMetalake", "testCatalog",
"testSchema", "missingView"));
+ Response response = (Response) interceptor.invoke(invocation);
+
+ assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
response.getStatus());
+ verify(invocation, never()).proceed();
+ verify(viewDispatcher).loadView(ArgumentMatchers.any());
+ Assertions.assertSame(view, created.get());
+ Assertions.assertTrue(response.getEntity() instanceof ErrorResponse);
+ assertEquals(
+ NoSuchViewException.class.getSimpleName(),
+ ((ErrorResponse) response.getEntity()).getType());
+
+ verify(mockEventBus, never()).dispatchEvent(ArgumentMatchers.any());
+ }
+ }
+
+ /**
+ * An absent table probe is terminal even when a concurrent create makes the
table loadable.
+ *
+ * @throws Throwable If authorization interception fails.
+ */
+ @Test
+ public void testMissingTableProbeDoesNotLoadConcurrentlyCreatedTable()
throws Throwable {
+ try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
+ MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+ mockStatic(GravitinoAuthorizerProvider.class);
+ MockedStatic<AuthorizationUtils> authUtilsMocked =
mockStatic(AuthorizationUtils.class);
+ MockedStatic<GravitinoEnv> envMocked = mockStatic(GravitinoEnv.class))
{
+ principalUtilsMocked
+ .when(PrincipalUtils::getCurrentPrincipal)
+ .thenReturn(new UserPrincipal("tester"));
+
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+
+ authUtilsMocked
+ .when(
+ () ->
+ AuthorizationUtils.checkCurrentUser(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation -> null);
+
+ GravitinoAuthorizerProvider mockedProvider =
mock(GravitinoAuthorizerProvider.class);
+ GravitinoAuthorizer authorizer = tableProbeAuthorizer();
+
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
+ when(mockedProvider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+ GravitinoEnv mockEnv = mock(GravitinoEnv.class);
+ TableDispatcher tableDispatcher = mock(TableDispatcher.class);
+ EventBus mockEventBus = spy(new EventBus(Collections.emptyList()));
+ envMocked.when(GravitinoEnv::getInstance).thenReturn(mockEnv);
+ when(mockEnv.internalTableDispatcher()).thenReturn(tableDispatcher);
+ when(mockEnv.eventBus()).thenReturn(mockEventBus);
+ AtomicReference<Table> created = new AtomicReference<>();
+ Table table = mock(Table.class);
+ when(table.name()).thenReturn("missingTable");
+ when(table.columns()).thenReturn(new Column[0]);
+ when(table.sortOrder()).thenReturn(new SortOrder[0]);
+ when(table.partitioning()).thenReturn(new Transform[0]);
+ when(table.index()).thenReturn(new Index[0]);
+ when(table.distribution()).thenReturn(Distributions.NONE);
+ when(table.properties()).thenReturn(Collections.emptyMap());
+ when(table.auditInfo())
+ .thenReturn(
+
AuditInfo.builder().withCreator("owner").withCreateTime(Instant.now()).build());
+ when(tableDispatcher.loadTable(ArgumentMatchers.any()))
+ .thenAnswer(
+ ignored -> {
+ if (created.get() == null) {
+ // The load observed absence; another request creates the
table before the handler
+ // can run.
+ created.set(table);
+ throw new NoSuchTableException("Table is absent");
+ }
+ return created.get();
+ });
+ TableOperations operations = new TableOperations(tableDispatcher);
+ FieldUtils.writeField(operations, "httpRequest",
mock(HttpServletRequest.class), true);
+ principalUtilsMocked
+ .when(() -> PrincipalUtils.doAs(any(), any()))
+ .thenAnswer(
+ invocation -> ((PrivilegedExceptionAction<?>)
invocation.getArgument(1)).run());
+
+ Method method =
+ TableOperations.class.getMethod(
+ "loadTable", String.class, String.class, String.class,
String.class, String.class);
+ MethodInterceptor interceptor =
+ new
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+ MethodInvocation invocation = mock(MethodInvocation.class);
+ when(invocation.getMethod()).thenReturn(method);
+ when(invocation.getArguments())
+ .thenReturn(
+ new Object[] {"testMetalake", "testCatalog", "testSchema",
"missingTable", null});
+
+ when(invocation.proceed())
+ .thenAnswer(
+ ignored ->
+ operations.loadTable(
+ "testMetalake", "testCatalog", "testSchema",
"missingTable", null));
+ Response response = (Response) interceptor.invoke(invocation);
+
+ assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
response.getStatus());
+ verify(invocation, never()).proceed();
+ verify(tableDispatcher).loadTable(ArgumentMatchers.any());
+ Assertions.assertSame(table, created.get());
+ Assertions.assertTrue(response.getEntity() instanceof ErrorResponse);
+ assertEquals(
+ NoSuchTableException.class.getSimpleName(),
+ ((ErrorResponse) response.getEntity()).getType());
+
+ verify(mockEventBus, never()).dispatchEvent(ArgumentMatchers.any());
+ }
+ }
+
/**
* When {@code checkCurrentUser} throws {@link NoSuchMetalakeException}, the
403 is a
* resource-not-found masquerading as forbidden — no {@link
AuthorizationDenialFailureEvent} is
@@ -984,7 +1254,8 @@ public class TestGravitinoInterceptionService {
ArgumentMatchers.eq("testMetalake"),
ArgumentMatchers.argThat(
metadataObject ->
- metadataObject.type() == MetadataObject.Type.CATALOG
+ metadataObject != null
+ && metadataObject.type() == MetadataObject.Type.CATALOG
&& "testCatalog".equals(metadataObject.name())),
ArgumentMatchers.eq(Privilege.Name.USE_CATALOG),
ArgumentMatchers.any()))
@@ -994,7 +1265,8 @@ public class TestGravitinoInterceptionService {
ArgumentMatchers.eq("testMetalake"),
ArgumentMatchers.argThat(
metadataObject ->
- metadataObject.type() == MetadataObject.Type.SCHEMA
+ metadataObject != null
+ && metadataObject.type() == MetadataObject.Type.SCHEMA
&& "testSchema".equals(metadataObject.name())),
ArgumentMatchers.eq(Privilege.Name.USE_SCHEMA),
ArgumentMatchers.any()))
@@ -1004,7 +1276,8 @@ public class TestGravitinoInterceptionService {
ArgumentMatchers.eq("testMetalake"),
ArgumentMatchers.argThat(
metadataObject ->
- metadataObject.type() == MetadataObject.Type.METALAKE
+ metadataObject != null
+ && metadataObject.type() ==
MetadataObject.Type.METALAKE
&& "testMetalake".equals(metadataObject.name())),
ArgumentMatchers.eq(Privilege.Name.CREATE_TABLE),
ArgumentMatchers.any()))
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
index 15bc76ce8f..6e58a850a4 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
@@ -20,17 +20,41 @@
package org.apache.gravitino.server.web.filter.authorization;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Collections;
+import java.util.Map;
import java.util.Optional;
+import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Entity;
+import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.catalog.TableDispatcher;
+import org.apache.gravitino.exceptions.NoSuchTableException;
+import org.apache.gravitino.rel.Table;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
import
org.apache.gravitino.server.authorization.annotations.ExpressionCondition;
import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
public class TestLoadTableAuthorizationExecutor {
private static final String PRIMARY_EXPRESSION =
@@ -63,6 +87,65 @@ public class TestLoadTableAuthorizationExecutor {
assertEquals(PRIMARY_EXPRESSION, expression(executor));
}
+ @Test
+ void testExistenceProbeUsesInternalTableDispatcher() throws Exception {
+ Method method = TestOperations.class.getMethod("loadTable", String.class);
+ NameIdentifier tableIdentifier = NameIdentifier.of("metalake", "catalog",
"schema", "table");
+ LoadTableAuthorizationExecutor executor =
+ new LoadTableAuthorizationExecutor(
+ method.getParameters(),
+ new Object[] {"SELECT_TABLE"},
+ PRIMARY_EXPRESSION,
+ Map.of(Entity.EntityType.TABLE, tableIdentifier),
+ Collections.emptyMap(),
+ Optional.empty(),
+ SECONDARY_EXPRESSION,
+ ExpressionCondition.NEVER,
+
AuthorizationExpressionConstants.PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION);
+
+ AuthorizationExpressionEvaluator primaryEvaluator =
+ mock(AuthorizationExpressionEvaluator.class);
+ AuthorizationExpressionEvaluator existenceEvaluator =
+ mock(AuthorizationExpressionEvaluator.class);
+ when(primaryEvaluator.evaluate(
+ anyMap(), anyMap(), any(AuthorizationRequestContext.class), any()))
+ .thenReturn(false);
+ when(existenceEvaluator.evaluate(
+ anyMap(), anyMap(), any(AuthorizationRequestContext.class), any()))
+ .thenReturn(true);
+ executor.authorizationExpressionEvaluator = primaryEvaluator;
+ FieldUtils.writeField(executor, "allowCheckExistenceEvaluator",
existenceEvaluator, true);
+
+ GravitinoEnv env = mock(GravitinoEnv.class);
+ TableDispatcher internalTableDispatcher = mock(TableDispatcher.class);
+ when(env.internalTableDispatcher()).thenReturn(internalTableDispatcher);
+ when(internalTableDispatcher.loadTable(tableIdentifier))
+ .thenThrow(new NoSuchTableException("Table is absent"));
+
+ try (MockedStatic<GravitinoEnv> mockedEnv =
mockStatic(GravitinoEnv.class)) {
+ mockedEnv.when(GravitinoEnv::getInstance).thenReturn(env);
+ assertThrows(
+ NoSuchTableException.class, () -> executor.execute(new
AuthorizationRequestContext()));
+
doReturn(mock(Table.class)).when(internalTableDispatcher).loadTable(tableIdentifier);
+ assertFalse(executor.execute(new AuthorizationRequestContext()));
+ IllegalStateException failure = new IllegalStateException("Table backend
unavailable");
+
doThrow(failure).when(internalTableDispatcher).loadTable(tableIdentifier);
+ assertSame(
+ failure,
+ assertThrows(
+ IllegalStateException.class,
+ () -> executor.execute(new AuthorizationRequestContext())));
+ verify(internalTableDispatcher, times(3)).loadTable(tableIdentifier);
+ reset(internalTableDispatcher);
+ when(existenceEvaluator.evaluate(anyMap(), anyMap(), any(),
any())).thenReturn(false);
+ assertFalse(executor.execute(new AuthorizationRequestContext()));
+ verifyNoInteractions(internalTableDispatcher);
+ when(primaryEvaluator.evaluate(anyMap(), anyMap(), any(),
any())).thenReturn(true);
+ assertTrue(executor.execute(new AuthorizationRequestContext()));
+ verifyNoInteractions(internalTableDispatcher);
+ }
+ }
+
private static LoadTableAuthorizationExecutor createExecutor(
String privileges, ExpressionCondition condition) throws Exception {
Method method = TestOperations.class.getMethod("loadTable", String.class);
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadViewAuthorizationExecutor.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadViewAuthorizationExecutor.java
new file mode 100644
index 0000000000..dbc767c4e1
--- /dev/null
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadViewAuthorizationExecutor.java
@@ -0,0 +1,170 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.server.web.filter.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import java.util.Optional;
+import javax.annotation.Nullable;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.catalog.ViewDispatcher;
+import org.apache.gravitino.exceptions.NoSuchViewException;
+import org.apache.gravitino.rel.View;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
+import
org.apache.gravitino.server.authorization.annotations.ExpressionCondition;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
+import org.apache.gravitino.server.web.rest.ViewOperations;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+/** Tests authorization and existence probing for view loads. */
+public class TestLoadViewAuthorizationExecutor {
+ @Test
+ void testAuthorizedMissingViewProbe() throws Exception {
+ assertThrows(NoSuchViewException.class, () -> check(false, true, false,
true, true));
+ }
+
+ @Test
+ void testExistingViewRemainsForbidden() throws Exception {
+ check(false, true, true, false, true);
+ }
+
+ @Test
+ void testNoProbePermissionDoesNotCheckExistence() throws Exception {
+ check(false, false, false, false, false);
+ }
+
+ @Test
+ void testPrimaryPermissionDoesNotProbe() throws Exception {
+ check(true, false, true, true, false);
+ }
+
+ @Test
+ void testViewEndpointSelectsProbeExecutor() throws Exception {
+ var method =
+ ViewOperations.class.getMethod(
+ "loadView", String.class, String.class, String.class,
String.class);
+ var annotation = method.getAnnotation(AuthorizationExpression.class);
+ var request =
method.getParameters()[3].getAnnotation(AuthorizationRequest.class);
+ assertEquals(AuthorizationRequest.RequestType.LOAD_VIEW, request.type());
+ assertEquals(
+
AuthorizationExpressionConstants.PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION,
+ annotation.allowCheckExistence());
+ assertInstanceOf(
+ LoadViewAuthorizationExecutor.class,
+ AuthorizeExecutorFactory.create(
+ annotation.expression(),
+ request.type(),
+ Map.of(),
+ Map.of(),
+ Optional.empty(),
+ method.getParameters(),
+ new Object[] {"m", "c", "s", "v"},
+ "",
+ ExpressionCondition.NEVER,
+ annotation.allowCheckExistence()));
+ }
+
+ @Test
+ void testUnsupportedViewOperationsRemainForbidden() throws Exception {
+ check(
+ false,
+ true,
+ false,
+ false,
+ true,
+ new UnsupportedOperationException("Catalog does not support view
operations"));
+ }
+
+ @Test
+ void testProbeBackendFailureIsNotTreatedAsMissingView() {
+ IllegalStateException failure = new IllegalStateException("View backend
unavailable");
+ assertSame(
+ failure,
+ assertThrows(
+ IllegalStateException.class, () -> check(false, true, false,
false, true, failure)));
+ }
+
+ private void check(
+ boolean primary, boolean probe, boolean exists, boolean expected,
boolean shouldProbe)
+ throws Exception {
+ check(primary, probe, exists, expected, shouldProbe, null);
+ }
+
+ private void check(
+ boolean primary,
+ boolean probe,
+ boolean exists,
+ boolean expected,
+ boolean shouldProbe,
+ @Nullable RuntimeException probeFailure)
+ throws Exception {
+ NameIdentifier ident = NameIdentifier.of("m", "c", "s", "v");
+ var executor =
+ new LoadViewAuthorizationExecutor(
+
AuthorizationExpressionConstants.LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+ Map.of(Entity.EntityType.VIEW, ident),
+ Map.of(),
+ Optional.empty(),
+
AuthorizationExpressionConstants.PROBE_TABLE_LIKE_AUTHORIZATION_EXPRESSION);
+ var primaryEvaluator = mock(AuthorizationExpressionEvaluator.class);
+ var probeEvaluator = mock(AuthorizationExpressionEvaluator.class);
+ when(primaryEvaluator.evaluate(anyMap(), anyMap(), any(),
any())).thenReturn(primary);
+ when(probeEvaluator.evaluate(anyMap(), anyMap(), any(),
any())).thenReturn(probe);
+ executor.authorizationExpressionEvaluator = primaryEvaluator;
+ FieldUtils.writeField(executor, "allowCheckExistenceEvaluator",
probeEvaluator, true);
+ var env = mock(GravitinoEnv.class);
+ var dispatcher = mock(ViewDispatcher.class);
+ when(env.internalViewDispatcher()).thenReturn(dispatcher);
+ if (probeFailure != null) {
+ when(dispatcher.loadView(ident)).thenThrow(probeFailure);
+ } else if (exists) {
+ when(dispatcher.loadView(ident)).thenReturn(mock(View.class));
+ } else {
+ when(dispatcher.loadView(ident)).thenThrow(new NoSuchViewException("View
is absent"));
+ }
+ try (MockedStatic<GravitinoEnv> mocked = mockStatic(GravitinoEnv.class)) {
+ mocked.when(GravitinoEnv::getInstance).thenReturn(env);
+ assertEquals(expected, executor.execute(new
AuthorizationRequestContext()));
+ }
+ if (shouldProbe) {
+ verify(dispatcher).loadView(ident);
+ } else {
+ verifyNoInteractions(dispatcher);
+ }
+ }
+}