This is an automated email from the ASF dual-hosted git repository.

roryqi 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 974da58a22 [#10902] feat(iceberg-rest): Split load table and view 
authorization expressions (#10918)
974da58a22 is described below

commit 974da58a221bfa25b6194df8a324248cf96399db
Author: roryqi <[email protected]>
AuthorDate: Fri May 29 10:12:29 2026 +0800

    [#10902] feat(iceberg-rest): Split load table and view authorization 
expressions (#10918)
    
    ### What changes were proposed in this pull request?
    
    This PR separates Iceberg REST load-table/load-view authorization into
    primary authorization expressions and secondary existence-check
    expressions.
    
    Main changes:
    - Add `allowCheckExistence` to `AuthorizationExpression` for load paths
    that need existence probing after primary authorization is denied.
    - Add `LoadViewAuthzHandler` and update `LoadTableAuthzHandler` to align
    handler behavior with the REST method annotations.
    - Split Iceberg table/view load and exists authorization constants.
    - Add `ExpressionCondition` for typed secondary-expression conditions
    and wire it through the Gravitino load-table authorization executor.
    - Add tests for Iceberg view authorization and load-table
    secondary-expression selection.
    
    ### Why are the changes needed?
    
    Iceberg clients may probe table and view endpoints while resolving
    relations. A single authorization expression makes it hard to
    distinguish real load authorization from existence checks, which can
    produce incorrect 403/404 behavior and block valid fallback flows
    between `/tables/` and `/views/`.
    
    Fix: #10902
    
    ### Does this PR introduce _any_ user-facing change?
    
    No public API change. This updates authorization behavior for Iceberg
    REST load table/view probes so clients get more accurate authorization
    and not-found responses.
    
    ### How was this patch tested?
    
    - Added unit tests for the new load-view authorization handler and
    expression behavior.
    - Added coverage for Gravitino load-table secondary-expression
    selection.
    - Added/updated Iceberg table authorization integration coverage.
    - Ran `./gradlew spotlessApply`.
    - Ran `./gradlew :server:test --tests
    
org.apache.gravitino.server.web.filter.authorization.TestLoadTableAuthorizationExecutor
    -PskipITs`.
---
 iceberg/iceberg-rest-server/build.gradle.kts       |   1 +
 .../service/rest/IcebergTableOperations.java       |  20 +--
 .../service/rest/IcebergViewOperations.java        |  28 +--
 .../service/rest/IcebergViewRenameOperations.java  |   6 +-
 .../annotations/IcebergAuthorizationMetadata.java  |   1 +
 ...BaseMetadataAuthorizationMethodInterceptor.java |   6 +-
 .../web/filter/IcebergLoadAuthzHandlerHelper.java  | 188 ++++++++++++++++++++
 ...bergMetadataAuthorizationMethodInterceptor.java |   9 +-
 .../server/web/filter/LoadTableAuthzHandler.java   | 160 +++++++----------
 .../server/web/filter/LoadViewAuthzHandler.java    | 126 +++++++++++++
 .../test/IcebergTableAuthorizationIT.java          |   9 +
 .../TestIcebergViewAuthorizationExpression.java    |  65 ++++++-
 .../filter/TestIcebergLoadAuthzHandlerHelper.java  | 196 +++++++++++++++++++++
 ...bergMetadataAuthorizationMethodInterceptor.java |   6 +-
 .../web/filter/TestLoadViewAuthzHandler.java       | 114 ++++++++++++
 .../annotations/AuthorizationExpression.java       |  13 +-
 .../annotations/ExpressionCondition.java           |  24 +++
 .../AuthorizationExpressionConstants.java          |  65 ++++++-
 .../web/filter/GravitinoInterceptionService.java   |   3 +-
 .../authorization/AuthorizeExecutorFactory.java    |   3 +-
 .../LoadTableAuthorizationExecutor.java            |  50 +++---
 .../gravitino/server/web/rest/TableOperations.java |   4 +-
 .../TestLoadTableAuthorizationExecutor.java        |  92 ++++++++++
 23 files changed, 1022 insertions(+), 167 deletions(-)

diff --git a/iceberg/iceberg-rest-server/build.gradle.kts 
b/iceberg/iceberg-rest-server/build.gradle.kts
index 6f2c73b8d0..60b0980987 100644
--- a/iceberg/iceberg-rest-server/build.gradle.kts
+++ b/iceberg/iceberg-rest-server/build.gradle.kts
@@ -108,6 +108,7 @@ dependencies {
   testImplementation(libs.junit.jupiter.api)
   testImplementation(libs.junit.jupiter.params)
   testImplementation(libs.mockito.core)
+  testImplementation(libs.mockito.inline)
   testImplementation(libs.mysql.driver)
   testImplementation(libs.postgresql.driver)
   testImplementation(libs.sqlite.jdbc)
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
index d537de2b8e..96c8a861ec 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
@@ -200,7 +200,7 @@ public class IcebergTableOperations {
       expression =
           "ANY(OWNER, METALAKE, CATALOG) || "
               + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA  && (TABLE::OWNER || 
ANY_MODIFY_TABLE)",
+              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && (TABLE::OWNER || 
ANY_MODIFY_TABLE)",
       accessMetadataType = MetadataObject.Type.TABLE)
   public Response updateTable(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
@@ -245,7 +245,7 @@ public class IcebergTableOperations {
       expression =
           "ANY(OWNER, METALAKE, CATALOG) || "
               + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA  && TABLE::OWNER ",
+              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && TABLE::OWNER ",
       accessMetadataType = MetadataObject.Type.TABLE)
   public Response dropTable(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
@@ -283,10 +283,11 @@ public class IcebergTableOperations {
   @Produces(MediaType.APPLICATION_JSON)
   @Timed(name = "load-table." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "load-table", absolute = true)
-  // SCHEMA-level authorization; TABLE-specific authorization is handled in 
LoadTableAuthzHandler
   @AuthorizationExpression(
-      expression = 
AuthorizationExpressionConstants.LOAD_SCHEMA_AUTHORIZATION_EXPRESSION,
-      accessMetadataType = MetadataObject.Type.SCHEMA)
+      expression = 
AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION,
+      allowCheckExistence =
+          
AuthorizationExpressionConstants.ICEBERG_TABLE_EXISTS_SECONDARY_AUTHORIZATION_EXPRESSION,
+      accessMetadataType = MetadataObject.Type.TABLE)
   public Response loadTable(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
@@ -351,10 +352,7 @@ public class IcebergTableOperations {
   @Timed(name = "table-exists." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "table-exists", absolute = true)
   @AuthorizationExpression(
-      expression =
-          "ANY(OWNER, METALAKE, CATALOG) || "
-              + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA  && (TABLE::OWNER || 
ANY_SELECT_TABLE || ANY_MODIFY_TABLE || ANY_CREATE_TABLE)",
+      expression = 
AuthorizationExpressionConstants.ICEBERG_TABLE_EXISTS_AUTHORIZATION_EXPRESSION,
       accessMetadataType = MetadataObject.Type.TABLE)
   public Response tableExists(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
@@ -450,7 +448,7 @@ public class IcebergTableOperations {
       expression =
           "ANY(OWNER, METALAKE, CATALOG) || "
               + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA  && (TABLE::OWNER || 
ANY_SELECT_TABLE || ANY_MODIFY_TABLE)",
+              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && (TABLE::OWNER || 
ANY_SELECT_TABLE || ANY_MODIFY_TABLE)",
       accessMetadataType = MetadataObject.Type.TABLE)
   public Response getTableCredentials(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
@@ -504,7 +502,7 @@ public class IcebergTableOperations {
       expression =
           "ANY(OWNER, METALAKE, CATALOG) || "
               + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA  && (TABLE::OWNER || 
ANY_SELECT_TABLE|| ANY_MODIFY_TABLE)",
+              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && (TABLE::OWNER || 
ANY_SELECT_TABLE || ANY_MODIFY_TABLE)",
       accessMetadataType = MetadataObject.Type.TABLE)
   public Response planTableScan(
       @PathParam("prefix") @AuthorizationMetadata(type = EntityType.CATALOG) 
String prefix,
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
index 2a456277dc..3685333162 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
@@ -54,6 +54,8 @@ import org.apache.gravitino.metrics.MetricNames;
 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.IcebergAuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata.RequestType;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
 import org.apache.gravitino.server.web.Utils;
 import org.apache.gravitino.utils.HierarchicalSchemaUtil;
@@ -126,10 +128,7 @@ public class IcebergViewOperations {
   @Timed(name = "create-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "create-view", absolute = true)
   @AuthorizationExpression(
-      expression =
-          "ANY(OWNER, METALAKE, CATALOG) || "
-              + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && ANY_CREATE_VIEW",
+      expression = 
AuthorizationExpressionConstants.ICEBERG_CREATE_VIEW_AUTHORIZATION_EXPRESSION,
       accessMetadataType = MetadataObject.Type.SCHEMA)
   public Response createView(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
@@ -165,12 +164,19 @@ public class IcebergViewOperations {
   @ResponseMetered(name = "load-view", absolute = true)
   @AuthorizationExpression(
       expression = 
AuthorizationExpressionConstants.ICEBERG_LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+      allowCheckExistence =
+          AuthorizationExpressionConstants
+              .ICEBERG_LOAD_VIEW_EXISTENCE_CHECK_AUTHORIZATION_EXPRESSION,
       accessMetadataType = MetadataObject.Type.VIEW)
   public Response loadView(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
       @AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded() 
@PathParam("namespace")
           String namespace,
-      @AuthorizationMetadata(type = EntityType.VIEW) @Encoded() 
@PathParam("view") String view) {
+      @IcebergAuthorizationMetadata(type = RequestType.LOAD_VIEW)
+          @AuthorizationMetadata(type = EntityType.VIEW)
+          @Encoded()
+          @PathParam("view")
+          String view) {
     String catalogName = IcebergRESTUtils.getCatalogName(prefix);
     Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
     String viewName = RESTUtil.decodeString(view);
@@ -201,10 +207,7 @@ public class IcebergViewOperations {
   @Timed(name = "replace-view." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "replace-view", absolute = true)
   @AuthorizationExpression(
-      expression =
-          "ANY(OWNER, METALAKE, CATALOG) || "
-              + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && VIEW::OWNER",
+      expression = 
AuthorizationExpressionConstants.ICEBERG_VIEW_OWNER_AUTHORIZATION_EXPRESSION,
       accessMetadataType = MetadataObject.Type.VIEW)
   public Response replaceView(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
@@ -243,10 +246,7 @@ public class IcebergViewOperations {
   @Timed(name = "drop-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "drop-view", absolute = true)
   @AuthorizationExpression(
-      expression =
-          "ANY(OWNER, METALAKE, CATALOG) || "
-              + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && VIEW::OWNER",
+      expression = 
AuthorizationExpressionConstants.ICEBERG_VIEW_OWNER_AUTHORIZATION_EXPRESSION,
       accessMetadataType = MetadataObject.Type.VIEW)
   public Response dropView(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
@@ -282,7 +282,7 @@ public class IcebergViewOperations {
   @Timed(name = "view-exists." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "view-exists", absolute = true)
   @AuthorizationExpression(
-      expression = 
AuthorizationExpressionConstants.ICEBERG_LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+      expression = 
AuthorizationExpressionConstants.ICEBERG_VIEW_EXISTS_AUTHORIZATION_EXPRESSION,
       accessMetadataType = MetadataObject.Type.VIEW)
   public Response viewExists(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewRenameOperations.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewRenameOperations.java
index 8014f37143..40c3f6da07 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewRenameOperations.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewRenameOperations.java
@@ -42,6 +42,7 @@ import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpres
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
 import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
 import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata.RequestType;
+import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
 import org.apache.gravitino.server.web.Utils;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
 import org.slf4j.Logger;
@@ -67,10 +68,7 @@ public class IcebergViewRenameOperations {
   @Timed(name = "rename-view." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "rename-view", absolute = true)
   @AuthorizationExpression(
-      expression =
-          "ANY(OWNER, METALAKE, CATALOG) || "
-              + "SCHEMA_OWNER_WITH_USE_CATALOG || "
-              + "ANY_USE_CATALOG && ANY_USE_SCHEMA && VIEW::OWNER",
+      expression = 
AuthorizationExpressionConstants.ICEBERG_VIEW_OWNER_AUTHORIZATION_EXPRESSION,
       accessMetadataType = MetadataObject.Type.VIEW)
   public Response renameView(
       @AuthorizationMetadata(type = Entity.EntityType.CATALOG) 
@PathParam("prefix") String prefix,
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/authorization/annotations/IcebergAuthorizationMetadata.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/authorization/annotations/IcebergAuthorizationMetadata.java
index 9ba92fab56..6488acb84f 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/authorization/annotations/IcebergAuthorizationMetadata.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/authorization/annotations/IcebergAuthorizationMetadata.java
@@ -30,6 +30,7 @@ import java.lang.annotation.Target;
 public @interface IcebergAuthorizationMetadata {
   enum RequestType {
     LOAD_TABLE,
+    LOAD_VIEW,
     RENAME_TABLE,
     RENAME_VIEW,
     CREATE_NAMESPACE,
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/BaseMetadataAuthorizationMethodInterceptor.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/BaseMetadataAuthorizationMethodInterceptor.java
index f2553751bd..d3ad8f0c73 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/BaseMetadataAuthorizationMethodInterceptor.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/BaseMetadataAuthorizationMethodInterceptor.java
@@ -89,13 +89,14 @@ public abstract class 
BaseMetadataAuthorizationMethodInterceptor implements Meth
    * <p>Override this method to provide custom handlers based on request 
characteristics (e.g.,
    * annotations, request types, parameters).
    *
+   * @param method REST method being invoked
    * @param parameters Method parameters
    * @param args Method arguments
    * @return Optional handler for custom authorization processing, or empty if 
standard
    *     authorization is sufficient
    */
   protected Optional<AuthorizationHandler> createAuthorizationHandler(
-      Parameter[] parameters, Object[] args) {
+      Method method, Parameter[] parameters, Object[] args) {
     return Optional.empty();
   }
 
@@ -160,7 +161,8 @@ public abstract class 
BaseMetadataAuthorizationMethodInterceptor implements Meth
         }
 
         // Process custom authorization if handler exists
-        Optional<AuthorizationHandler> handler = 
createAuthorizationHandler(parameters, args);
+        Optional<AuthorizationHandler> handler =
+            createAuthorizationHandler(method, parameters, args);
 
         if (!skipStandardCheck && handler.isPresent()) {
           AuthorizationHandler authzHandler = handler.get();
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergLoadAuthzHandlerHelper.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergLoadAuthzHandlerHelper.java
new file mode 100644
index 0000000000..3f18f82d6c
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergLoadAuthzHandlerHelper.java
@@ -0,0 +1,188 @@
+/*
+ *  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;
+
+import java.lang.reflect.Parameter;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.BooleanSupplier;
+import java.util.function.Supplier;
+import org.apache.gravitino.Entity.EntityType;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.AuthorizationRequestContext;
+import org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
+import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata.RequestType;
+import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
+import org.apache.gravitino.utils.PrincipalUtils;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.rest.RESTUtil;
+
+/** Shared helper for Iceberg load table/view authorization handlers. */
+final class IcebergLoadAuthzHandlerHelper {
+
+  private IcebergLoadAuthzHandlerHelper() {}
+
+  static LoadTarget extractLoadTarget(
+      Parameter[] parameters, Object[] args, RequestType requestType) {
+    String targetName = null;
+    Namespace namespace = null;
+
+    for (int i = 0; i < parameters.length; i++) {
+      Parameter parameter = parameters[i];
+
+      IcebergAuthorizationMetadata icebergMetadata =
+          parameter.getAnnotation(IcebergAuthorizationMetadata.class);
+      if (icebergMetadata != null && icebergMetadata.type() == requestType) {
+        targetName = RESTUtil.decodeString(String.valueOf(args[i]));
+      }
+
+      AuthorizationMetadata authMetadata = 
parameter.getAnnotation(AuthorizationMetadata.class);
+      if (authMetadata != null && authMetadata.type() == EntityType.SCHEMA) {
+        namespace = RESTUtil.decodeNamespace(String.valueOf(args[i]));
+      }
+    }
+
+    return new LoadTarget(targetName, namespace);
+  }
+
+  static LoadContext extractLoadContext(
+      Map<EntityType, NameIdentifier> nameIdentifierMap,
+      Supplier<RuntimeException> missingContextException) {
+    NameIdentifier catalogId = nameIdentifierMap.get(EntityType.CATALOG);
+    NameIdentifier schemaId = nameIdentifierMap.get(EntityType.SCHEMA);
+
+    if (catalogId == null || schemaId == null) {
+      throw missingContextException.get();
+    }
+
+    return new LoadContext(catalogId.namespace().level(0), catalogId.name(), 
schemaId.name());
+  }
+
+  static IcebergCatalogWrapper getCatalogWrapper(String catalog) {
+    return IcebergRESTServerContext.getInstance()
+        .catalogWrapperManager()
+        .getCatalogWrapper(catalog);
+  }
+
+  static String resolveExpression(
+      AuthorizationExpression authorizationExpression, String 
defaultExpression) {
+    if (authorizationExpression != null
+        && authorizationExpression.expression() != null
+        && !authorizationExpression.expression().isBlank()) {
+      return authorizationExpression.expression();
+    }
+    return defaultExpression;
+  }
+
+  static String resolveAllowCheckExistenceExpression(
+      AuthorizationExpression authorizationExpression, String 
defaultExpression) {
+    if (authorizationExpression != null) {
+      String expr = authorizationExpression.allowCheckExistence();
+      if (expr != null && !expr.isBlank()) {
+        return expr;
+      }
+    }
+    return defaultExpression;
+  }
+
+  static void authorizeLoadEntity(
+      Map<EntityType, NameIdentifier> nameIdentifierMap,
+      String primaryExpression,
+      String allowCheckExistenceExpression,
+      BooleanSupplier exists,
+      Supplier<RuntimeException> notFoundException,
+      String entityType,
+      NameIdentifier entityId) {
+    Map<String, Object> emptyPathParams = Collections.emptyMap();
+    AuthorizationRequestContext requestContext = new 
AuthorizationRequestContext();
+    Optional<String> emptyEntityType = Optional.empty();
+
+    AuthorizationExpressionEvaluator primaryEvaluator =
+        new AuthorizationExpressionEvaluator(primaryExpression);
+    if (primaryEvaluator.evaluate(
+        nameIdentifierMap, emptyPathParams, requestContext, emptyEntityType)) {
+      return;
+    }
+
+    AuthorizationExpressionEvaluator allowExistenceEvaluator =
+        new AuthorizationExpressionEvaluator(allowCheckExistenceExpression);
+    if (allowExistenceEvaluator.evaluate(
+            nameIdentifierMap, emptyPathParams, requestContext, 
emptyEntityType)
+        && !exists.getAsBoolean()) {
+      throw notFoundException.get();
+    }
+
+    String currentUser = PrincipalUtils.getCurrentUserName();
+    throw new ForbiddenException(
+        "User '%s' is not authorized to load %s '%s'", currentUser, 
entityType, entityId);
+  }
+
+  static final class LoadTarget {
+    private final String name;
+    private final Namespace namespace;
+
+    private LoadTarget(String name, Namespace namespace) {
+      this.name = name;
+      this.namespace = namespace;
+    }
+
+    boolean hasMissingPart() {
+      return name == null || namespace == null;
+    }
+
+    String name() {
+      return name;
+    }
+
+    Namespace namespace() {
+      return namespace;
+    }
+  }
+
+  static final class LoadContext {
+    private final String metalakeName;
+    private final String catalog;
+    private final String schema;
+
+    private LoadContext(String metalakeName, String catalog, String schema) {
+      this.metalakeName = metalakeName;
+      this.catalog = catalog;
+      this.schema = schema;
+    }
+
+    String metalakeName() {
+      return metalakeName;
+    }
+
+    String catalog() {
+      return catalog;
+    }
+
+    String schema() {
+      return schema;
+    }
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
index 197ff36256..43ab6d6c03 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.server.web.filter;
 
+import java.lang.reflect.Method;
 import java.lang.reflect.Parameter;
 import java.util.HashMap;
 import java.util.Map;
@@ -31,6 +32,7 @@ import 
org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
 import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
 import org.apache.gravitino.iceberg.service.IcebergRESTUtils;
 import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
 import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
 import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata.RequestType;
@@ -109,7 +111,8 @@ public class IcebergMetadataAuthorizationMethodInterceptor
    */
   @Override
   protected Optional<AuthorizationHandler> createAuthorizationHandler(
-      Parameter[] parameters, Object[] args) {
+      Method method, Parameter[] parameters, Object[] args) {
+    AuthorizationExpression authExpr = 
method.getAnnotation(AuthorizationExpression.class);
     for (Parameter parameter : parameters) {
       IcebergAuthorizationMetadata icebergMetadata =
           parameter.getAnnotation(IcebergAuthorizationMetadata.class);
@@ -118,7 +121,9 @@ public class IcebergMetadataAuthorizationMethodInterceptor
         RequestType type = icebergMetadata.type();
         switch (type) {
           case LOAD_TABLE:
-            return Optional.of(new LoadTableAuthzHandler(parameters, args));
+            return Optional.of(new LoadTableAuthzHandler(authExpr, parameters, 
args));
+          case LOAD_VIEW:
+            return Optional.of(new LoadViewAuthzHandler(authExpr, parameters, 
args));
           case RENAME_TABLE:
             return Optional.of(new RenameTableAuthzHandler(parameters, args));
           case RENAME_VIEW:
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/LoadTableAuthzHandler.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/LoadTableAuthzHandler.java
index 178ce2524a..f715b52ed0 100644
--- 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/LoadTableAuthzHandler.java
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/LoadTableAuthzHandler.java
@@ -20,112 +20,82 @@
 package org.apache.gravitino.server.web.filter;
 
 import java.lang.reflect.Parameter;
-import java.util.HashMap;
 import java.util.Map;
-import java.util.Optional;
 import org.apache.gravitino.Entity.EntityType;
 import org.apache.gravitino.NameIdentifier;
-import org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
-import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
-import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
-import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
-import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
 import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata.RequestType;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
-import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
 import 
org.apache.gravitino.server.web.filter.BaseMetadataAuthorizationMethodInterceptor.AuthorizationHandler;
+import 
org.apache.gravitino.server.web.filter.IcebergLoadAuthzHandlerHelper.LoadContext;
+import 
org.apache.gravitino.server.web.filter.IcebergLoadAuthzHandlerHelper.LoadTarget;
 import org.apache.gravitino.utils.NameIdentifierUtil;
-import org.apache.gravitino.utils.PrincipalUtils;
 import org.apache.iceberg.MetadataTableType;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableIdentifier;
-import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.exceptions.NoSuchTableException;
-import org.apache.iceberg.rest.RESTUtil;
 
 /**
- * Handler for LOAD_TABLE operations. Validates that the requested entity is 
not a metadata table or
- * a view, and extracts the table identifier for authorization checks.
+ * Handler for LOAD_TABLE operations. Validates that the requested entity is 
not a metadata table,
+ * rejects identifiers that resolve to views (so clients can fall back to 
{@code /views/}), and
+ * performs authorization aligned with the REST {@link 
AuthorizationExpression} on {@code
+ * loadTable}.
  *
- * <p>Per Iceberg REST spec, the /tables/ endpoint should only serve tables. 
If the identifier
- * refers to a view, this handler throws NoSuchTableException (404), 
triggering Spark to retry with
- * the /views/ endpoint.
+ * <p>Authorization follows {@link
+ * 
org.apache.gravitino.iceberg.service.rest.IcebergTableOperations#loadTable}: 
primary {@code
+ * LOAD_TABLE} expression from {@link AuthorizationExpression#expression()}, 
then {@link
+ * AuthorizationExpression#allowCheckExistence()} when the primary denies 
(existence-probe path for
+ * clients such as Trino/Spark).
  */
 public class LoadTableAuthzHandler implements AuthorizationHandler {
+  private final AuthorizationExpression authorizationExpression;
   private final Parameter[] parameters;
   private final Object[] args;
 
-  public LoadTableAuthzHandler(Parameter[] parameters, Object[] args) {
+  public LoadTableAuthzHandler(
+      AuthorizationExpression authorizationExpression, Parameter[] parameters, 
Object[] args) {
+    this.authorizationExpression = authorizationExpression;
     this.parameters = parameters;
     this.args = args;
   }
 
   @Override
   public void process(Map<EntityType, NameIdentifier> nameIdentifierMap) {
-    // Find the table name and namespace from parameters
-    String tableName = null;
-    Namespace namespace = null;
-
-    for (int i = 0; i < parameters.length; i++) {
-      Parameter parameter = parameters[i];
-
-      IcebergAuthorizationMetadata icebergMetadata =
-          parameter.getAnnotation(IcebergAuthorizationMetadata.class);
-      if (icebergMetadata != null && icebergMetadata.type() == 
RequestType.LOAD_TABLE) {
-        // TODO: Refactor to move decode logic to interceptor in a generic way
-        // See: 
https://docs.google.com/document/d/18yx88tBbU3S9LB8hhL7xUVzSWHIWkXJ7sRNmLh2v_kQ/
-        // Consider consolidating custom authorization handlers and 
standardizing parameter decoding
-        tableName = RESTUtil.decodeString(String.valueOf(args[i]));
-      }
-
-      AuthorizationMetadata authMetadata = 
parameter.getAnnotation(AuthorizationMetadata.class);
-      if (authMetadata != null && authMetadata.type() == EntityType.SCHEMA) {
-        // Decode the raw Iceberg namespace parameter
-        namespace = RESTUtil.decodeNamespace(String.valueOf(args[i]));
-      }
-    }
+    LoadTarget loadTarget =
+        IcebergLoadAuthzHandlerHelper.extractLoadTarget(parameters, args, 
RequestType.LOAD_TABLE);
 
-    if (tableName == null || namespace == null) {
+    if (loadTarget.hasMissingPart()) {
       throw new NoSuchTableException("Table not found - missing table name or 
namespace");
     }
 
+    String tableName = loadTarget.name();
+    Namespace namespace = loadTarget.namespace();
+
     // Validate that this is not a metadata table access
     if (isMetadataTable(tableName, namespace)) {
       throw new NoSuchTableException("Table %s not found", tableName);
     }
 
-    NameIdentifier catalogId = nameIdentifierMap.get(EntityType.CATALOG);
-    NameIdentifier schemaId = nameIdentifierMap.get(EntityType.SCHEMA);
-
-    if (catalogId == null || schemaId == null) {
-      throw new NoSuchTableException("Missing catalog or schema context for 
table authorization");
-    }
-
-    String metalakeName = catalogId.namespace().level(0);
-    String catalog = catalogId.name();
-    String schema = schemaId.name();
+    LoadContext loadContext =
+        IcebergLoadAuthzHandlerHelper.extractLoadContext(
+            nameIdentifierMap,
+            () ->
+                new NoSuchTableException(
+                    "Missing catalog or schema context for table 
authorization"));
 
     // Per Iceberg REST spec, /tables/ endpoint should only serve tables, not 
views.
-    // 1. Check if it's a view first -> 404 (enables Spark fallback to /views/)
-    // 2. Authorize the table access -> 403 if unauthorized
-    // 3. Let request proceed - the actual loadTable() call will handle 
non-existence
-    IcebergCatalogWrapperManager wrapperManager =
-        IcebergRESTServerContext.getInstance().catalogWrapperManager();
-    IcebergCatalogWrapper catalogWrapper = 
wrapperManager.getCatalogWrapper(catalog);
+    // 1. Authorize the table access (see performTableAuthorization) -> 403 if 
unauthorized
+    // 2. Let request proceed - the actual loadTable() call will handle 
non-existence
+    IcebergCatalogWrapper catalogWrapper =
+        IcebergLoadAuthzHandlerHelper.getCatalogWrapper(loadContext.catalog());
     TableIdentifier tableIdentifier = TableIdentifier.of(namespace, tableName);
 
-    // Only check view existence when the catalog supports view operations. 
Catalogs backed by
-    // JDBC without jdbc.schema-version=V1 throw UnsupportedOperationException 
from viewExists(),
-    // which would surface as a 500 error. When view operations are 
unsupported the identifier
-    // cannot be a view, so we skip the check entirely.
-    if (catalogWrapper.supportsViewOperations() && 
catalogWrapper.viewExists(tableIdentifier)) {
-      throw new NoSuchTableException("Table %s not found", tableName);
-    }
-
-    nameIdentifierMap.put(
-        EntityType.TABLE, NameIdentifierUtil.ofTable(metalakeName, catalog, 
schema, tableName));
-    performTableAuthorization(nameIdentifierMap);
+    NameIdentifier tableId =
+        NameIdentifierUtil.ofTable(
+            loadContext.metalakeName(), loadContext.catalog(), 
loadContext.schema(), tableName);
+    nameIdentifierMap.put(EntityType.TABLE, tableId);
+    performTableAuthorization(nameIdentifierMap, catalogWrapper, 
tableIdentifier, tableId);
   }
 
   @Override
@@ -135,27 +105,36 @@ public class LoadTableAuthzHandler implements 
AuthorizationHandler {
   }
 
   /**
-   * Perform TABLE-level authorization check using 
ICEBERG_LOAD_TABLE_AUTHORIZATION_EXPRESSION. This
-   * enforces table-specific privileges including ANY_CREATE_TABLE for Iceberg 
REST.
+   * Perform TABLE-level authorization aligned with {@link
+   * 
org.apache.gravitino.iceberg.service.rest.IcebergTableOperations#loadTable}: 
evaluate the
+   * primary expression first; if it denies, evaluate {@link
+   * AuthorizationExpression#allowCheckExistence()} when non-blank. Before 
primary expression
+   * evaluation, if the catalog supports views and the identifier resolves to 
a view, throws {@link
+   * NoSuchTableException} so clients can fall back to {@code /views/} (see 
Iceberg REST spec for
+   * {@code /tables/}).
    */
-  private void performTableAuthorization(Map<EntityType, NameIdentifier> 
nameIdentifierMap) {
-    AuthorizationExpressionEvaluator evaluator =
-        new AuthorizationExpressionEvaluator(
-            
AuthorizationExpressionConstants.ICEBERG_LOAD_TABLE_AUTHORIZATION_EXPRESSION);
-
-    boolean authorized =
-        evaluator.evaluate(
-            nameIdentifierMap,
-            new HashMap<>(),
-            new AuthorizationRequestContext(),
-            Optional.empty());
-
-    if (!authorized) {
-      String currentUser = PrincipalUtils.getCurrentUserName();
-      NameIdentifier tableId = nameIdentifierMap.get(EntityType.TABLE);
-      throw new ForbiddenException(
-          "User '%s' is not authorized to load table '%s'", currentUser, 
tableId);
+  private void performTableAuthorization(
+      Map<EntityType, NameIdentifier> nameIdentifierMap,
+      IcebergCatalogWrapper catalogWrapper,
+      TableIdentifier tableIdentifier,
+      NameIdentifier tableId) {
+    if (catalogWrapper.supportsViewOperations() && 
catalogWrapper.viewExists(tableIdentifier)) {
+      throw new NoSuchTableException("Table %s not found", 
tableIdentifier.name());
     }
+
+    IcebergLoadAuthzHandlerHelper.authorizeLoadEntity(
+        nameIdentifierMap,
+        IcebergLoadAuthzHandlerHelper.resolveExpression(
+            authorizationExpression,
+            
AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION),
+        IcebergLoadAuthzHandlerHelper.resolveAllowCheckExistenceExpression(
+            authorizationExpression,
+            AuthorizationExpressionConstants
+                .ICEBERG_TABLE_EXISTS_SECONDARY_AUTHORIZATION_EXPRESSION),
+        () -> catalogWrapper.tableExists(tableIdentifier),
+        () -> new NoSuchTableException("Table %s not found", 
tableIdentifier.name()),
+        "table",
+        tableId);
   }
 
   /**
@@ -173,11 +152,8 @@ public class LoadTableAuthzHandler implements 
AuthorizationHandler {
       return false;
     }
 
-    // Metadata tables have namespace length > 1 (e.g., catalog.db.table has 3 
levels)
-    // Regular tables have namespace length = 1 (e.g., catalog.db has 2 
levels, but we get "db")
-    if (namespace.levels().length > 1) {
-      return true;
-    }
-    return false;
+    // Metadata tables have namespace length > 1 (e.g., catalog.db.table has 3 
levels).
+    // Regular tables have namespace length = 1 (e.g., catalog.db has 2 
levels, but we get "db").
+    return namespace.levels().length > 1;
   }
 }
diff --git 
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/LoadViewAuthzHandler.java
 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/LoadViewAuthzHandler.java
new file mode 100644
index 0000000000..304046e270
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/LoadViewAuthzHandler.java
@@ -0,0 +1,126 @@
+/*
+ *  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;
+
+import java.lang.reflect.Parameter;
+import java.util.Map;
+import org.apache.gravitino.Entity.EntityType;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.iceberg.common.ops.IcebergCatalogWrapper;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata.RequestType;
+import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import 
org.apache.gravitino.server.web.filter.BaseMetadataAuthorizationMethodInterceptor.AuthorizationHandler;
+import 
org.apache.gravitino.server.web.filter.IcebergLoadAuthzHandlerHelper.LoadContext;
+import 
org.apache.gravitino.server.web.filter.IcebergLoadAuthzHandlerHelper.LoadTarget;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.NoSuchViewException;
+
+/**
+ * Handler for LOAD_VIEW operations. Validates the view identifier and 
performs authorization
+ * aligned with {@link 
org.apache.gravitino.iceberg.service.rest.IcebergViewOperations#loadView}.
+ *
+ * <p>Authorization follows the REST method: primary {@code ICEBERG_LOAD_VIEW} 
from {@link
+ * AuthorizationExpression#expression()}, then non-blank {@link
+ * AuthorizationExpression#allowCheckExistence()} when the primary denies.
+ */
+public class LoadViewAuthzHandler implements AuthorizationHandler {
+  private final AuthorizationExpression authorizationExpression;
+  private final Parameter[] parameters;
+  private final Object[] args;
+
+  public LoadViewAuthzHandler(
+      AuthorizationExpression authorizationExpression, Parameter[] parameters, 
Object[] args) {
+    this.authorizationExpression = authorizationExpression;
+    this.parameters = parameters;
+    this.args = args;
+  }
+
+  @Override
+  public void process(Map<EntityType, NameIdentifier> nameIdentifierMap) {
+    LoadTarget loadTarget =
+        IcebergLoadAuthzHandlerHelper.extractLoadTarget(parameters, args, 
RequestType.LOAD_VIEW);
+
+    if (loadTarget.hasMissingPart()) {
+      throw new NoSuchViewException("View not found - missing view name or 
namespace");
+    }
+
+    String viewName = loadTarget.name();
+    Namespace namespace = loadTarget.namespace();
+    LoadContext loadContext =
+        IcebergLoadAuthzHandlerHelper.extractLoadContext(
+            nameIdentifierMap,
+            () ->
+                new NoSuchViewException(
+                    "Missing catalog or schema context for view 
authorization"));
+
+    IcebergCatalogWrapper catalogWrapper =
+        IcebergLoadAuthzHandlerHelper.getCatalogWrapper(loadContext.catalog());
+    TableIdentifier viewIdentifier = TableIdentifier.of(namespace, viewName);
+
+    NameIdentifier viewId =
+        NameIdentifierUtil.ofView(
+            loadContext.metalakeName(), loadContext.catalog(), 
loadContext.schema(), viewName);
+    nameIdentifierMap.put(EntityType.VIEW, viewId);
+    // Spark probes loadView(name) before falling back to loadTable(name), so 
the existence-check
+    // expression grants the existence probe to principals holding privileges 
on a like-named
+    // table. Register a TABLE identifier under the same name so that 
expression has a TABLE entity
+    // to evaluate against; this is what lets a table owner get 404 (not 403) 
for an absent view.
+    nameIdentifierMap.put(
+        EntityType.TABLE,
+        NameIdentifierUtil.ofTable(
+            loadContext.metalakeName(), loadContext.catalog(), 
loadContext.schema(), viewName));
+
+    performViewAuthorization(nameIdentifierMap, catalogWrapper, 
viewIdentifier, viewId);
+  }
+
+  @Override
+  public boolean authorizationCompleted() {
+    return true;
+  }
+
+  /**
+   * Perform VIEW-level authorization aligned with {@link
+   * 
org.apache.gravitino.iceberg.service.rest.IcebergViewOperations#loadView}: 
evaluate the primary
+   * expression first; if it denies, evaluate {@link 
AuthorizationExpression#allowCheckExistence()}
+   * when non-blank.
+   */
+  private void performViewAuthorization(
+      Map<EntityType, NameIdentifier> nameIdentifierMap,
+      IcebergCatalogWrapper catalogWrapper,
+      TableIdentifier viewIdentifier,
+      NameIdentifier viewId) {
+    IcebergLoadAuthzHandlerHelper.authorizeLoadEntity(
+        nameIdentifierMap,
+        IcebergLoadAuthzHandlerHelper.resolveExpression(
+            authorizationExpression,
+            
AuthorizationExpressionConstants.ICEBERG_LOAD_VIEW_AUTHORIZATION_EXPRESSION),
+        IcebergLoadAuthzHandlerHelper.resolveAllowCheckExistenceExpression(
+            authorizationExpression,
+            AuthorizationExpressionConstants
+                .ICEBERG_LOAD_VIEW_EXISTENCE_CHECK_AUTHORIZATION_EXPRESSION),
+        () -> catalogWrapper.viewExists(viewIdentifier),
+        () -> new NoSuchViewException("View %s not found", 
viewIdentifier.name()),
+        "view",
+        viewId);
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
index cea3f1acce..0d19211fc7 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/integration/test/IcebergTableAuthorizationIT.java
@@ -282,6 +282,15 @@ public class IcebergTableAuthorizationIT extends 
IcebergAuthorizationIT {
         ForbiddenException.class, () -> sql("DESC TABLE %s_not_exist", 
tableName));
   }
 
+  @Test
+  void testSelectTableAsTableOwnerWithUseCatalogAndUseSchema() {
+    String tableName = "test_select_table_owner_with_use";
+    createTable(SCHEMA_NAME, tableName);
+    setTableOwner(tableName);
+
+    Assertions.assertDoesNotThrow(() -> sql("SELECT * FROM %s", tableName));
+  }
+
   @Test
   void testDropTable() {
     String tableName = "test_drop";
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
index b341fbc4fa..44bd419f4e 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/authorization/TestIcebergViewAuthorizationExpression.java
@@ -19,16 +19,20 @@
 
 package org.apache.gravitino.iceberg.service.rest.authorization;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.collect.ImmutableSet;
 import java.lang.reflect.Field;
 import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
 import ognl.OgnlException;
 import org.apache.gravitino.iceberg.service.rest.IcebergViewOperations;
 import org.apache.gravitino.iceberg.service.rest.IcebergViewRenameOperations;
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
 import org.apache.iceberg.rest.requests.CreateViewRequest;
 import org.apache.iceberg.rest.requests.RenameTableRequest;
@@ -181,11 +185,68 @@ public class TestIcebergViewAuthorizationExpression {
                 "METALAKE::USE_SCHEMA",
                 "METALAKE::USE_CATALOG")));
 
-    // CREATE_VIEW grants load permission to allow catalog implementations 
that call viewExists()
-    // during creation to work
+    // CREATE_VIEW alone does NOT grant the primary load-view permission; 
existence checks for
+    // view creation are handled by the allowCheckExistence expression -- see
+    // {@link
+    // 
AuthorizationExpressionConstants#ICEBERG_LOAD_VIEW_EXISTENCE_CHECK_AUTHORIZATION_EXPRESSION}
+    // and {@link 
#testIcebergLoadViewAllowCheckExistenceAuthorizationExpression()} below.
+    assertFalse(
+        mockEvaluator.getResult(
+            ImmutableSet.of("SCHEMA::CREATE_VIEW", "SCHEMA::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
+  }
+
+  @Test
+  public void testIcebergLoadViewAllowCheckExistenceAuthorizationExpression() 
throws OgnlException {
+    // Existence-check expression used by LoadViewAuthzHandler when the 
primary load-view expression
+    // denies: should allow schema-level principals with create-view or 
table-access privileges to
+    // resolve a view identifier (existence check) even without full load-view 
privileges.
+    String expression =
+        
AuthorizationExpressionConstants.ICEBERG_LOAD_VIEW_EXISTENCE_CHECK_AUTHORIZATION_EXPRESSION;
+    MockAuthorizationExpressionEvaluator mockEvaluator =
+        new MockAuthorizationExpressionEvaluator(expression);
+
+    // No privileges -> denied.
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of()));
+
+    // CREATE_VIEW + USE_SCHEMA + USE_CATALOG -> allowed: lets catalog 
implementations that call
+    // viewExists() during creation succeed.
     assertTrue(
         mockEvaluator.getResult(
             ImmutableSet.of("SCHEMA::CREATE_VIEW", "SCHEMA::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
+
+    // Table-side grants also pass the secondary check (used for resolving 
view backing tables).
+    assertTrue(
+        mockEvaluator.getResult(
+            ImmutableSet.of("SCHEMA::SELECT_TABLE", "SCHEMA::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
+    assertTrue(
+        mockEvaluator.getResult(
+            ImmutableSet.of("SCHEMA::CREATE_TABLE", "SCHEMA::USE_SCHEMA", 
"CATALOG::USE_CATALOG")));
+
+    // Missing USE_SCHEMA -> denied.
+    assertFalse(
+        mockEvaluator.getResult(ImmutableSet.of("SCHEMA::CREATE_VIEW", 
"CATALOG::USE_CATALOG")));
+    // Missing USE_CATALOG -> denied.
+    assertFalse(
+        mockEvaluator.getResult(ImmutableSet.of("SCHEMA::CREATE_VIEW", 
"SCHEMA::USE_SCHEMA")));
+  }
+
+  @Test
+  public void testLoadViewIcebergAuthorizationMetadata() throws 
NoSuchMethodException {
+    Method method =
+        IcebergViewOperations.class.getMethod("loadView", String.class, 
String.class, String.class);
+    AuthorizationExpression annotation = 
method.getAnnotation(AuthorizationExpression.class);
+    assertEquals(
+        
AuthorizationExpressionConstants.ICEBERG_LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+        annotation.expression());
+    assertEquals(
+        
AuthorizationExpressionConstants.ICEBERG_LOAD_VIEW_EXISTENCE_CHECK_AUTHORIZATION_EXPRESSION,
+        annotation.allowCheckExistence());
+
+    Parameter viewParameter = method.getParameters()[2];
+    IcebergAuthorizationMetadata icebergMetadata =
+        viewParameter.getAnnotation(IcebergAuthorizationMetadata.class);
+    assertNotNull(icebergMetadata);
+    assertEquals(IcebergAuthorizationMetadata.RequestType.LOAD_VIEW, 
icebergMetadata.type());
   }
 
   @Test
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergLoadAuthzHandlerHelper.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergLoadAuthzHandlerHelper.java
new file mode 100644
index 0000000000..f675986510
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergLoadAuthzHandlerHelper.java
@@ -0,0 +1,196 @@
+/*
+ *  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;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.function.BooleanSupplier;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.authorization.GravitinoAuthorizer;
+import org.apache.gravitino.server.authorization.GravitinoAuthorizerProvider;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
+import 
org.apache.gravitino.server.web.filter.IcebergLoadAuthzHandlerHelper.LoadTarget;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.PrincipalUtils;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.exceptions.NoSuchViewException;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+/** Test for {@link IcebergLoadAuthzHandlerHelper}. */
+public class TestIcebergLoadAuthzHandlerHelper {
+
+  private static final String METALAKE = "test_metalake";
+  private static final String CATALOG = "test_catalog";
+  private static final String SCHEMA = "test_schema";
+  private static final String NAME = "probe_target";
+
+  // Models the loadView probe Spark issues before falling back to loadTable: 
the primary
+  // expression requires view ownership, while allowCheckExistence lets a 
table owner probe. The
+  // three branches of authorizeLoadEntity decide whether such a caller sees 
the entity, a 404, or
+  // a 403.
+  private static final String PRIMARY_EXPRESSION = "VIEW::OWNER";
+  private static final String ALLOW_CHECK_EXISTENCE_EXPRESSION = 
"TABLE::OWNER";
+
+  @Test
+  public void testPrimaryGrantAllows() {
+    // View owner: primary grants regardless of existence, so the load is 
authorized.
+    assertDoesNotThrow(
+        () -> runAuthorizeLoadEntity(true, false, false),
+        "view owner must be authorized even when the existence check would 
deny");
+  }
+
+  @Test
+  public void testAbsentProbeNotFound() {
+    // Table owner probing a view that does not exist: primary denies, 
existence check allows, and
+    // the view is absent, so the caller gets 404 instead of 403 (the bug this 
PR fixes, which lets
+    // Spark fall back to loadTable).
+    assertThrows(NoSuchViewException.class, () -> 
runAuthorizeLoadEntity(false, true, false));
+  }
+
+  @Test
+  public void testPresentProbeForbidden() {
+    // Table owner probing a view that actually exists: the existence check 
must not leak access to
+    // a real view the caller has no privilege on, so it stays 403.
+    assertThrows(ForbiddenException.class, () -> runAuthorizeLoadEntity(false, 
true, true));
+  }
+
+  @Test
+  public void testNoAccessForbidden() {
+    // Caller satisfies neither expression: denied with 403 without probing 
existence.
+    assertThrows(ForbiddenException.class, () -> runAuthorizeLoadEntity(false, 
false, false));
+  }
+
+  private void runAuthorizeLoadEntity(
+      boolean primaryGranted, boolean existenceCheckGranted, boolean exists) {
+    try (MockedStatic<PrincipalUtils> principalUtils = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> providerStatic =
+            mockStatic(GravitinoAuthorizerProvider.class)) {
+      principalUtils
+          .when(PrincipalUtils::getCurrentPrincipal)
+          .thenReturn(new UserPrincipal("tester"));
+      
principalUtils.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+
+      GravitinoAuthorizer authorizer = mock(GravitinoAuthorizer.class);
+      when(authorizer.isOwner(
+              any(),
+              any(),
+              argThat(object -> object != null && object.type() == 
MetadataObject.Type.VIEW),
+              any()))
+          .thenReturn(primaryGranted);
+      when(authorizer.isOwner(
+              any(),
+              any(),
+              argThat(object -> object != null && object.type() == 
MetadataObject.Type.TABLE),
+              any()))
+          .thenReturn(existenceCheckGranted);
+
+      GravitinoAuthorizerProvider provider = 
mock(GravitinoAuthorizerProvider.class);
+      when(provider.getGravitinoAuthorizer()).thenReturn(authorizer);
+      
providerStatic.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+
+      Map<Entity.EntityType, NameIdentifier> nameIdentifierMap = new 
HashMap<>();
+      nameIdentifierMap.put(Entity.EntityType.METALAKE, 
NameIdentifierUtil.ofMetalake(METALAKE));
+      nameIdentifierMap.put(
+          Entity.EntityType.VIEW, NameIdentifierUtil.ofView(METALAKE, CATALOG, 
SCHEMA, NAME));
+      nameIdentifierMap.put(
+          Entity.EntityType.TABLE, NameIdentifierUtil.ofTable(METALAKE, 
CATALOG, SCHEMA, NAME));
+
+      BooleanSupplier existsSupplier = () -> exists;
+      IcebergLoadAuthzHandlerHelper.authorizeLoadEntity(
+          nameIdentifierMap,
+          PRIMARY_EXPRESSION,
+          ALLOW_CHECK_EXISTENCE_EXPRESSION,
+          existsSupplier,
+          () -> new NoSuchViewException("View %s not found", NAME),
+          "view",
+          NameIdentifierUtil.ofView(METALAKE, CATALOG, SCHEMA, NAME));
+    }
+  }
+
+  @Test
+  public void testExtractLoadTarget() throws Exception {
+    Method method =
+        TestOperations.class.getMethod("loadTable", String.class, 
String.class, String.class);
+
+    LoadTarget loadTarget =
+        IcebergLoadAuthzHandlerHelper.extractLoadTarget(
+            method.getParameters(),
+            new Object[] {"test_catalog/", "test_schema", "orders%20table"},
+            IcebergAuthorizationMetadata.RequestType.LOAD_TABLE);
+
+    assertEquals("orders table", loadTarget.name());
+    assertEquals(Namespace.of("test_schema"), loadTarget.namespace());
+  }
+
+  @Test
+  public void testResolveExpression() throws Exception {
+    Method method =
+        TestOperations.class.getMethod("loadTable", String.class, 
String.class, String.class);
+    AuthorizationExpression authorizationExpression =
+        method.getAnnotation(AuthorizationExpression.class);
+
+    assertEquals(
+        "custom_primary",
+        IcebergLoadAuthzHandlerHelper.resolveExpression(
+            authorizationExpression, "default_primary"));
+    assertEquals(
+        "custom_existence",
+        IcebergLoadAuthzHandlerHelper.resolveAllowCheckExistenceExpression(
+            authorizationExpression, "default_existence"));
+    assertEquals(
+        "default_primary",
+        IcebergLoadAuthzHandlerHelper.resolveExpression(null, 
"default_primary"));
+    assertEquals(
+        "default_existence",
+        IcebergLoadAuthzHandlerHelper.resolveAllowCheckExistenceExpression(
+            null, "default_existence"));
+  }
+
+  @SuppressWarnings("unused")
+  public static class TestOperations {
+    @AuthorizationExpression(
+        expression = "custom_primary",
+        allowCheckExistence = "custom_existence",
+        accessMetadataType = MetadataObject.Type.TABLE)
+    public void loadTable(
+        @AuthorizationMetadata(type = Entity.EntityType.CATALOG) String prefix,
+        @AuthorizationMetadata(type = Entity.EntityType.SCHEMA) String 
namespace,
+        @IcebergAuthorizationMetadata(type = 
IcebergAuthorizationMetadata.RequestType.LOAD_TABLE)
+            @AuthorizationMetadata(type = Entity.EntityType.TABLE)
+            String table) {}
+  }
+}
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
index fa675cad4b..b33b85bb8e 100644
--- 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
@@ -268,7 +268,7 @@ public class 
TestIcebergMetadataAuthorizationMethodInterceptor {
         new IcebergMetadataAuthorizationMethodInterceptor() {
           @Override
           protected Optional<AuthorizationHandler> createAuthorizationHandler(
-              Parameter[] parameters, Object[] args) {
+              Method method, Parameter[] parameters, Object[] args) {
             return Optional.of(
                 new AuthorizationHandler() {
                   @Override
@@ -316,7 +316,7 @@ public class 
TestIcebergMetadataAuthorizationMethodInterceptor {
         new IcebergMetadataAuthorizationMethodInterceptor() {
           @Override
           protected Optional<AuthorizationHandler> createAuthorizationHandler(
-              Parameter[] parameters, Object[] args) {
+              Method method, Parameter[] parameters, Object[] args) {
             return Optional.of(
                 new AuthorizationHandler() {
                   @Override
@@ -394,7 +394,7 @@ public class 
TestIcebergMetadataAuthorizationMethodInterceptor {
         new IcebergMetadataAuthorizationMethodInterceptor() {
           @Override
           protected Optional<AuthorizationHandler> createAuthorizationHandler(
-              Parameter[] parameters, Object[] args) {
+              Method method, Parameter[] parameters, Object[] args) {
             throw new RuntimeException("test");
           }
         };
diff --git 
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestLoadViewAuthzHandler.java
 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestLoadViewAuthzHandler.java
new file mode 100644
index 0000000000..94b5e87369
--- /dev/null
+++ 
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestLoadViewAuthzHandler.java
@@ -0,0 +1,114 @@
+/*
+ *  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;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import java.lang.reflect.Method;
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.iceberg.service.CatalogWrapperForREST;
+import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
+import 
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
+import org.apache.gravitino.iceberg.service.provider.IcebergConfigProvider;
+import org.apache.gravitino.server.ServerConfig;
+import org.apache.gravitino.server.authorization.GravitinoAuthorizerProvider;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.annotations.IcebergAuthorizationMetadata;
+import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+/** Test for {@link LoadViewAuthzHandler}. */
+public class TestLoadViewAuthzHandler {
+
+  private static final String TEST_METALAKE = "test_metalake";
+  private static final String TEST_CATALOG = "test_catalog";
+  private static final String TEST_SCHEMA = "test_schema";
+
+  @BeforeAll
+  public static void initAuthorizer() {
+    GravitinoAuthorizerProvider.getInstance().initialize(new ServerConfig());
+  }
+
+  @Test
+  public void testLoadViewDoesNotRejectMetadataTableLikeName() throws 
Exception {
+    IcebergCatalogWrapperManager wrapperManager = 
Mockito.mock(IcebergCatalogWrapperManager.class);
+    CatalogWrapperForREST catalogWrapper = 
Mockito.mock(CatalogWrapperForREST.class);
+    
when(wrapperManager.getCatalogWrapper(TEST_CATALOG)).thenReturn(catalogWrapper);
+    resetContext(wrapperManager);
+
+    Method method =
+        TestOperations.class.getMethod("loadView", String.class, String.class, 
String.class);
+    LoadViewAuthzHandler handler =
+        new LoadViewAuthzHandler(
+            method.getAnnotation(AuthorizationExpression.class),
+            method.getParameters(),
+            new Object[] {TEST_CATALOG + "/", "nested.table", "files"});
+
+    Map<Entity.EntityType, NameIdentifier> nameIdentifierMap = new HashMap<>();
+    nameIdentifierMap.put(Entity.EntityType.METALAKE, 
NameIdentifierUtil.ofMetalake(TEST_METALAKE));
+    nameIdentifierMap.put(
+        Entity.EntityType.CATALOG, NameIdentifierUtil.ofCatalog(TEST_METALAKE, 
TEST_CATALOG));
+    nameIdentifierMap.put(
+        Entity.EntityType.SCHEMA,
+        NameIdentifierUtil.ofSchema(TEST_METALAKE, TEST_CATALOG, TEST_SCHEMA));
+
+    assertDoesNotThrow(() -> handler.process(nameIdentifierMap));
+    verify(catalogWrapper, never()).viewExists(any(TableIdentifier.class));
+  }
+
+  private static void resetContext(IcebergCatalogWrapperManager 
wrapperManager) {
+    IcebergConfigProvider configProvider = 
Mockito.mock(IcebergConfigProvider.class);
+    when(configProvider.getMetalakeName()).thenReturn(TEST_METALAKE);
+    when(configProvider.getDefaultCatalogName()).thenReturn(TEST_CATALOG);
+    IcebergRESTServerContext.create(configProvider, false, false, true, 
wrapperManager);
+  }
+
+  /** Test operations class to provide method annotations for testing. */
+  @SuppressWarnings("unused")
+  public static class TestOperations {
+    @AuthorizationExpression(
+        expression = 
AuthorizationExpressionConstants.ICEBERG_LOAD_VIEW_AUTHORIZATION_EXPRESSION,
+        allowCheckExistence =
+            AuthorizationExpressionConstants
+                .ICEBERG_LOAD_VIEW_EXISTENCE_CHECK_AUTHORIZATION_EXPRESSION,
+        accessMetadataType = MetadataObject.Type.VIEW)
+    public void loadView(
+        @AuthorizationMetadata(type = Entity.EntityType.CATALOG) String prefix,
+        @AuthorizationMetadata(type = Entity.EntityType.SCHEMA) String 
namespace,
+        @IcebergAuthorizationMetadata(type = 
IcebergAuthorizationMetadata.RequestType.LOAD_VIEW)
+            @AuthorizationMetadata(type = Entity.EntityType.VIEW)
+            String view) {
+      // Test method
+    }
+  }
+}
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java
index 2e772b4ce3..05a6b32bf4 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/AuthorizationExpression.java
@@ -64,5 +64,16 @@ public @interface AuthorizationExpression {
    *
    * @return the condition for evaluating the secondary expression.
    */
-  String secondaryExpressionCondition() default "";
+  ExpressionCondition secondaryExpressionCondition() default 
ExpressionCondition.NEVER;
+
+  /**
+   * Optional expression evaluated after the primary {@link #expression()} 
denies authorization, to
+   * decide whether the caller may probe object existence (for example Iceberg 
REST load probes that
+   * distinguish 403 from 404). This is separate from {@link 
#secondaryExpression()} so load paths
+   * do not overload secondary for unrelated semantics (such as MODIFY_TABLE 
privilege switching on
+   * the Gravitino core server).
+   *
+   * @return expression that permits existence checks when primary denies
+   */
+  String allowCheckExistence() default "";
 }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/ExpressionCondition.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/ExpressionCondition.java
new file mode 100644
index 0000000000..e811871217
--- /dev/null
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/annotations/ExpressionCondition.java
@@ -0,0 +1,24 @@
+/*
+ * 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.authorization.annotations;
+
+public enum ExpressionCondition {
+  NEVER,
+  REQUIRED_MODIFY_PRIVILEGES
+}
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
index bdb13b8ba8..122bdfcd01 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/expression/AuthorizationExpressionConstants.java
@@ -48,6 +48,26 @@ public class AuthorizationExpressionConstants {
                   ANY_USE_CATALOG && ANY_USE_SCHEMA  && (TABLE::OWNER || 
ANY_SELECT_TABLE || ANY_MODIFY_TABLE || ANY_CREATE_TABLE)
                   """;
 
+  /**
+   * Existence-check expression for Iceberg REST {@code loadTable}: used when 
the primary load-table
+   * expression is forbidden, to allow schema-level principals who can 
create/select views to
+   * resolve a table identifier (e.g. view backing table) without full table 
privileges.
+   */
+  public static final String 
ICEBERG_TABLE_EXISTS_SECONDARY_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA  && (ANY_CREATE_TABLE || 
ANY_SELECT_VIEW || ANY_CREATE_VIEW || VIEW::OWNER)
+                  """;
+
+  /** Iceberg REST {@code HEAD .../tables/{table}} (table exists). */
+  public static final String ICEBERG_TABLE_EXISTS_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY(OWNER, METALAKE, CATALOG) ||
+                  SCHEMA_OWNER_WITH_USE_CATALOG ||
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA  && (TABLE::OWNER || 
VIEW::OWNER ||
+                  ANY_CREATE_VIEW || ANY_SELECT_VIEW || ANY_SELECT_TABLE ||
+                  ANY_MODIFY_TABLE || ANY_CREATE_TABLE)
+                  """;
+
   public static final String MODIFY_TABLE_AUTHORIZATION_EXPRESSION =
       """
                   ANY(OWNER, METALAKE, CATALOG) ||
@@ -97,16 +117,46 @@ public class AuthorizationExpressionConstants {
                   ANY_USE_CATALOG && ANY_USE_SCHEMA && (VIEW::OWNER || 
ANY_SELECT_VIEW || ANY_CREATE_VIEW)
                   """;
 
-  //  Adding ANY_SELECT_TABLE / ANY_MODIFY_TABLE / ANY_CREATE_TABLE here 
because Spark probes
-  //  viewExists(tableName) when resolving any relation. Without table 
privileges in the
-  //  expression, users who only hold table grants get a spurious 403 on the 
HEAD /views/{name}
-  //  probe, blocking legitimate table reads.
   public static final String ICEBERG_LOAD_VIEW_AUTHORIZATION_EXPRESSION =
       """
                   ANY(OWNER, METALAKE, CATALOG) ||
                   SCHEMA_OWNER_WITH_USE_CATALOG ||
-                  ANY_USE_CATALOG && ANY_USE_SCHEMA && (VIEW::OWNER || 
ANY_SELECT_VIEW || ANY_CREATE_VIEW
-                      || ANY_SELECT_TABLE || ANY_MODIFY_TABLE || 
ANY_CREATE_TABLE)
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA && (VIEW::OWNER || 
ANY_SELECT_VIEW)
+                  """;
+
+  /**
+   * Existence-check expression for Iceberg REST {@code loadView}: when the 
primary load-view
+   * expression is forbidden, allows schema-level principals with view or 
referenced-table paths
+   * (similar shape to {@link 
#ICEBERG_TABLE_EXISTS_SECONDARY_AUTHORIZATION_EXPRESSION}, tuned for
+   * view metadata and backing relation resolution).
+   */
+  public static final String 
ICEBERG_LOAD_VIEW_EXISTENCE_CHECK_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA  &&
+                  (ANY_CREATE_VIEW || TABLE::OWNER || ANY_SELECT_TABLE || 
ANY_MODIFY_TABLE || ANY_CREATE_TABLE)
+                  """;
+
+  public static final String ICEBERG_CREATE_VIEW_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY(OWNER, METALAKE, CATALOG) ||
+                  SCHEMA_OWNER_WITH_USE_CATALOG ||
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA  && ANY_CREATE_VIEW
+                  """;
+
+  /** Iceberg REST replace view, drop view, and rename view (VIEW::OWNER 
path). */
+  public static final String ICEBERG_VIEW_OWNER_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY(OWNER, METALAKE, CATALOG) ||
+                  SCHEMA_OWNER_WITH_USE_CATALOG ||
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA  && VIEW::OWNER
+                  """;
+
+  /** Iceberg REST {@code HEAD .../views/{view}} (view exists). */
+  public static final String ICEBERG_VIEW_EXISTS_AUTHORIZATION_EXPRESSION =
+      """
+                  ANY(OWNER, METALAKE, CATALOG) ||
+                  SCHEMA_OWNER_WITH_USE_CATALOG ||
+                  ANY_USE_CATALOG && ANY_USE_SCHEMA  && (VIEW::OWNER || 
ANY_SELECT_VIEW || ANY_CREATE_VIEW)
                   """;
 
   public static final String FILTER_TABLE_AUTHORIZATION_EXPRESSION =
@@ -186,7 +236,4 @@ public class AuthorizationExpressionConstants {
 
   public static final String LOAD_JOB_TEMPLATE_AUTHORIZATION_EXPRESSION =
       "METALAKE::OWNER || JOB_TEMPLATE::OWNER || ANY_USE_JOB_TEMPLATE";
-
-  public static final String REQUEST_REQUIRED_PRIVILEGES_CONTAINS_MODIFY_TABLE 
=
-      "REQUEST::REQUIRED_PRIVILEGES_CONTAINS_MODIFY_TABLE";
 }
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 0f542bdeb9..0f229d85a4 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
@@ -46,6 +46,7 @@ import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 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.web.Utils;
 import 
org.apache.gravitino.server.web.filter.authorization.AuthorizationExecutor;
 import 
org.apache.gravitino.server.web.filter.authorization.AuthorizeExecutorFactory;
@@ -190,7 +191,7 @@ public class GravitinoInterceptionService implements 
InterceptionService {
             AuthorizationRequest.RequestType requestType =
                 extractAuthorizationRequestTypeFromParameters(parameters);
             String secondaryExpression = 
expressionAnnotation.secondaryExpression();
-            String secondaryExpressionCondition =
+            ExpressionCondition secondaryExpressionCondition =
                 expressionAnnotation.secondaryExpressionCondition();
             executor =
                 AuthorizeExecutorFactory.create(
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 5badd704d4..4d2f4c63aa 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
@@ -23,6 +23,7 @@ import java.util.Optional;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.NameIdentifier;
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
+import 
org.apache.gravitino.server.authorization.annotations.ExpressionCondition;
 
 public class AuthorizeExecutorFactory {
 
@@ -35,7 +36,7 @@ public class AuthorizeExecutorFactory {
       Parameter[] parameters,
       Object[] args,
       String secondaryExpression,
-      String secondaryExpressionCondition) {
+      ExpressionCondition secondaryExpressionCondition) {
     return switch (requestType) {
       case COMMON -> new CommonAuthorizerExecutor(
           expression, metadataContext, pathParams, entityType);
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 caa6a7b312..5b87979518 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
@@ -20,13 +20,11 @@ import java.lang.reflect.Parameter;
 import java.util.Arrays;
 import java.util.Map;
 import java.util.Optional;
-import java.util.Set;
-import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.Privilege;
-import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
+import 
org.apache.gravitino.server.authorization.annotations.ExpressionCondition;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
 import org.apache.gravitino.server.web.filter.ParameterUtil;
 
@@ -52,30 +50,36 @@ public class LoadTableAuthorizationExecutor extends 
CommonAuthorizerExecutor {
       Map<String, Object> pathParams,
       Optional<String> entityType,
       String secondaryExpression,
-      String secondaryExpressionCondition) {
+      ExpressionCondition secondaryExpressionCondition) {
     super(expression, metadataContext, pathParams, entityType);
 
-    // If secondaryExpression and condition are provided, evaluate the 
condition
-    if (StringUtils.isNotBlank(secondaryExpression)
-        && StringUtils.isNotBlank(secondaryExpressionCondition)) {
-      String privileges = (String) 
ParameterUtil.extractFromParameters(parameters, args);
+    if (!shouldCheckModifyTablePrivilege(secondaryExpression, 
secondaryExpressionCondition)) {
+      return;
+    }
+
+    String privileges = (String) 
ParameterUtil.extractFromParameters(parameters, args);
+    if (containsModifyTablePrivilege(privileges)) {
+      this.expression = secondaryExpression;
+      this.authorizationExpressionEvaluator =
+          new AuthorizationExpressionEvaluator(secondaryExpression);
+    }
+  }
 
-      // Evaluate the condition: does the request contain MODIFY_TABLE 
privilege?
-      if (privileges != null
-          && secondaryExpressionCondition.equals(
-              
AuthorizationExpressionConstants.REQUEST_REQUIRED_PRIVILEGES_CONTAINS_MODIFY_TABLE))
 {
-        Set<Privilege.Name> privilegeNames =
-            Arrays.stream(privileges.split(","))
-                .map(Privilege.Name::valueOf)
-                .collect(Collectors.toSet());
+  private static boolean shouldCheckModifyTablePrivilege(
+      String secondaryExpression, ExpressionCondition 
secondaryExpressionCondition) {
+    return StringUtils.isNotBlank(secondaryExpression)
+        && secondaryExpressionCondition == 
ExpressionCondition.REQUIRED_MODIFY_PRIVILEGES;
+  }
 
-        if (privilegeNames.contains(Privilege.Name.MODIFY_TABLE)) {
-          // Use the secondary expression for stricter authorization
-          this.expression = secondaryExpression;
-          this.authorizationExpressionEvaluator =
-              new AuthorizationExpressionEvaluator(secondaryExpression);
-        }
-      }
+  private static boolean containsModifyTablePrivilege(String privileges) {
+    if (StringUtils.isBlank(privileges)) {
+      return false;
     }
+
+    return Arrays.stream(privileges.split(","))
+        .map(String::trim)
+        .filter(StringUtils::isNotBlank)
+        .map(Privilege.Name::valueOf)
+        .anyMatch(Privilege.Name.MODIFY_TABLE::equals);
   }
 }
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
index 0940abb458..fa89ee0259 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/TableOperations.java
@@ -55,6 +55,7 @@ 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.annotations.ExpressionCondition;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
 import org.apache.gravitino.server.web.Utils;
 import org.apache.gravitino.utils.NameIdentifierUtil;
@@ -167,8 +168,7 @@ public class TableOperations {
   @AuthorizationExpression(
       expression = 
AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION,
       secondaryExpression = 
AuthorizationExpressionConstants.MODIFY_TABLE_AUTHORIZATION_EXPRESSION,
-      secondaryExpressionCondition =
-          
AuthorizationExpressionConstants.REQUEST_REQUIRED_PRIVILEGES_CONTAINS_MODIFY_TABLE,
+      secondaryExpressionCondition = 
ExpressionCondition.REQUIRED_MODIFY_PRIVILEGES,
       accessMetadataType = MetadataObject.Type.TABLE)
   public Response loadTable(
       @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
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
new file mode 100644
index 0000000000..fcd0f273e7
--- /dev/null
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/filter/authorization/TestLoadTableAuthorizationExecutor.java
@@ -0,0 +1,92 @@
+/*
+ * 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 java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.Optional;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+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.junit.jupiter.api.Test;
+
+public class TestLoadTableAuthorizationExecutor {
+  private static final String PRIMARY_EXPRESSION =
+      AuthorizationExpressionConstants.LOAD_TABLE_AUTHORIZATION_EXPRESSION;
+  private static final String SECONDARY_EXPRESSION =
+      AuthorizationExpressionConstants.MODIFY_TABLE_AUTHORIZATION_EXPRESSION;
+
+  @Test
+  public void testUsesSecondaryExpressionForRequiredModifyPrivilege() throws 
Exception {
+    LoadTableAuthorizationExecutor executor =
+        createExecutor(
+            "SELECT_TABLE, MODIFY_TABLE", 
ExpressionCondition.REQUIRED_MODIFY_PRIVILEGES);
+
+    assertEquals(SECONDARY_EXPRESSION, expression(executor));
+  }
+
+  @Test
+  public void testUsesPrimaryExpressionWhenConditionNever() throws Exception {
+    LoadTableAuthorizationExecutor executor =
+        createExecutor("MODIFY_TABLE", ExpressionCondition.NEVER);
+
+    assertEquals(PRIMARY_EXPRESSION, expression(executor));
+  }
+
+  @Test
+  public void testUsesPrimaryExpressionWithoutModifyPrivilege() throws 
Exception {
+    LoadTableAuthorizationExecutor executor =
+        createExecutor("SELECT_TABLE", 
ExpressionCondition.REQUIRED_MODIFY_PRIVILEGES);
+
+    assertEquals(PRIMARY_EXPRESSION, expression(executor));
+  }
+
+  private static LoadTableAuthorizationExecutor createExecutor(
+      String privileges, ExpressionCondition condition) throws Exception {
+    Method method = TestOperations.class.getMethod("loadTable", String.class);
+    return new LoadTableAuthorizationExecutor(
+        method.getParameters(),
+        new Object[] {privileges},
+        PRIMARY_EXPRESSION,
+        Collections.<Entity.EntityType, NameIdentifier>emptyMap(),
+        Collections.emptyMap(),
+        Optional.empty(),
+        SECONDARY_EXPRESSION,
+        condition);
+  }
+
+  private static String expression(LoadTableAuthorizationExecutor executor) 
throws Exception {
+    Field expressionField = 
CommonAuthorizerExecutor.class.getDeclaredField("expression");
+    expressionField.setAccessible(true);
+    return (String) expressionField.get(executor);
+  }
+
+  @SuppressWarnings("unused")
+  public static class TestOperations {
+    public void loadTable(
+        @AuthorizationRequest(type = 
AuthorizationRequest.RequestType.LOAD_TABLE)
+            String privileges) {}
+  }
+}


Reply via email to