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

roryqi 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 405fcdad3e [#14610] fix(authz): Return 400 for invalid requests on 
branch-1.3 (#14613)
405fcdad3e is described below

commit 405fcdad3e0c1eb9f5cf0245007ca89bffa02fc7
Author: Jerry Shao <[email protected]>
AuthorDate: Thu Nov 5 09:41:48 2026 +0800

    [#14610] fix(authz): Return 400 for invalid requests on branch-1.3 (#14613)
    
    ### What changes were proposed in this pull request?
    
    Adapt the authorization validation fixes from #14612 to branch-1.3:
    - Return HTTP 400 for invalid metadata object names and privileges.
    - Validate all privileges without short-circuiting.
    - Return HTTP 400 for null or invalid tag/policy association bodies.
    
    Preserve HTTP 500 for unexpected internal errors.
    
    ### Why are the changes needed?
    
    Invalid client inputs are incorrectly reported as authorization internal
    errors. Null tag/policy association bodies return 403 instead of 400.
    
    Fixes #14610
    
    ### Does this PR introduce _any_ user-facing change?
    
    Affected invalid requests return HTTP 400 with the validation error
    instead of HTTP 500 or 403.
    
    ### How was this patch tested?
    
    - Added regression coverage for tag/policy association bodies, invalid
    object names, and invalid privileges.
    - Confirmed empty association bodies returned 500 before the fix.
    - Passed 103 targeted authorization and REST tests.
    - Ran `./gradlew spotlessApply` and `git diff --check`.
---
 .../web/filter/GravitinoInterceptionService.java   |   6 +-
 .../gravitino/server/web/filter/ParameterUtil.java |   7 +-
 .../server/web/filter/RequestValidation.java       |  75 ++++++++
 .../AssociatePolicyAuthorizationExecutor.java      |   6 +-
 .../AssociateTagAuthorizationExecutor.java         |   6 +-
 .../LineageAuthorizationExecutor.java              |   8 +-
 .../LoadTableAuthorizationExecutor.java            |  11 +-
 .../authorization/RunJobAuthorizationExecutor.java |   9 +-
 .../filter/TestGravitinoInterceptionService.java   | 200 +++++++++++++++++++++
 9 files changed, 304 insertions(+), 24 deletions(-)

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 e5440fdd70..47fa28e110 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
@@ -256,9 +256,6 @@ public class GravitinoInterceptionService implements 
InterceptionService {
                   viewIdent.name(),
                   NameIdentifierUtil.getSchemaIdentifier(viewIdent).name(),
                   exception);
-            } catch (BadRequestException exception) {
-              LOG.warn("Invalid authorization request", exception);
-              return Utils.illegalArguments(exception.getMessage(), exception);
             }
             if (!authorizeResult) {
               MetadataObject.Type type = 
expressionAnnotation.accessMetadataType();
@@ -280,6 +277,9 @@ public class GravitinoInterceptionService implements 
InterceptionService {
           }
         }
         return methodInvocation.proceed();
+      } catch (BadRequestException ex) {
+        LOG.warn("Invalid authorization request", ex);
+        return Utils.illegalArguments(ex.getMessage(), ex);
       } catch (IllegalMetadataObjectException ex) {
         LOG.warn("Invalid metadata object type during authorization", ex);
         return Utils.illegalArguments(
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
index 026a5a47f3..eede162869 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/ParameterUtil.java
@@ -90,8 +90,11 @@ public class ParameterUtil {
         } catch (IllegalArgumentException e) {
           throw new IllegalMetadataObjectException(e, "Invalid metadata object 
type: %s", rawType);
         }
-        NameIdentifier nameIdentifier =
-            MetadataObjectUtil.toEntityIdent(metalake, 
MetadataObjects.parse(fullName.get(), type));
+        MetadataObject object =
+            RequestValidation.parse(
+                () -> MetadataObjects.parse(fullName.get(), type),
+                e -> new IllegalMetadataObjectException(e, "%s", 
e.getMessage()));
+        NameIdentifier nameIdentifier = 
MetadataObjectUtil.toEntityIdent(metalake, object);
         nameIdentifierMap.putAll(
             NameIdentifierUtil.splitNameIdentifier(
                 metalake, MetadataObjectUtil.toEntityType(type), 
nameIdentifier));
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/RequestValidation.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/RequestValidation.java
new file mode 100644
index 0000000000..418091f924
--- /dev/null
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/RequestValidation.java
@@ -0,0 +1,75 @@
+/*
+ * 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.util.function.Function;
+import java.util.function.Supplier;
+import org.apache.gravitino.exceptions.BadRequestException;
+
+/** Converts failures from explicit client-input validation into request 
errors. */
+public final class RequestValidation {
+
+  private RequestValidation() {}
+
+  /**
+   * Validates client input without including authorization or backend 
operations.
+   *
+   * @param validation the client-input validation to perform
+   * @throws BadRequestException if validation rejects the input
+   */
+  public static void validate(Runnable validation) {
+    parse(
+        () -> {
+          validation.run();
+          return null;
+        });
+  }
+
+  /**
+   * Parses client input and reports invalid arguments as a bad request.
+   *
+   * @param parser the client-input parser, excluding configuration and 
authorization operations
+   * @param <T> the parsed result type
+   * @return the parsed result
+   * @throws BadRequestException if parsing rejects the input
+   */
+  public static <T> T parse(Supplier<T> parser) {
+    return parse(parser, e -> new BadRequestException(e, "%s", 
e.getMessage()));
+  }
+
+  /**
+   * Parses client input while preserving the caller's request-error type and 
message.
+   *
+   * <p>Only explicit client-input parsing belongs in this scope. Authorizer 
construction,
+   * configuration loading, expression evaluation, and backend calls must 
remain outside it.
+   *
+   * @param parser the client-input parser
+   * @param errorMapper converts an input validation failure into the 
appropriate request error
+   * @param <T> the parsed result type
+   * @return the parsed result
+   */
+  public static <T> T parse(
+      Supplier<T> parser,
+      Function<IllegalArgumentException, ? extends RuntimeException> 
errorMapper) {
+    try {
+      return parser.get();
+    } catch (IllegalArgumentException e) {
+      throw errorMapper.apply(e);
+    }
+  }
+}
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
index 59b2e83305..6e32c7c891 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociatePolicyAuthorizationExecutor.java
@@ -29,6 +29,8 @@ import org.apache.gravitino.Entity;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.dto.requests.PoliciesAssociateRequest;
+import org.apache.gravitino.exceptions.BadRequestException;
+import org.apache.gravitino.server.web.filter.RequestValidation;
 
 /**
  * Metadata object authorization for {@link
@@ -57,7 +59,7 @@ public class AssociatePolicyAuthorizationExecutor extends 
CommonAuthorizerExecut
   public boolean execute(AuthorizationRequestContext context) throws Exception 
{
     Object request = extractFromParameters(parameters, args);
     if (request == null) {
-      return false;
+      throw new BadRequestException("Request body cannot be null");
     }
 
     context.setOriginalAuthorizationExpression(expression);
@@ -67,7 +69,7 @@ public class AssociatePolicyAuthorizationExecutor extends 
CommonAuthorizerExecut
         request instanceof PoliciesAssociateRequest,
         "Only policy can use AssociatePolicyAuthorizationExecutor, please 
contact the administrator.");
     PoliciesAssociateRequest policiesAssociateRequest = 
(PoliciesAssociateRequest) request;
-    policiesAssociateRequest.validate();
+    RequestValidation.validate(policiesAssociateRequest::validate);
     // Authorize both 'policiesToAdd' and 'policiesToRemove' fields.
     return authorizePolicy(policiesAssociateRequest.getPoliciesToAdd(), 
context, targetType)
         && authorizePolicy(policiesAssociateRequest.getPoliciesToRemove(), 
context, targetType);
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
index 2fef4e9ee3..0535889f11 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/AssociateTagAuthorizationExecutor.java
@@ -29,6 +29,8 @@ import org.apache.gravitino.Entity;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.dto.requests.TagsAssociateRequest;
+import org.apache.gravitino.exceptions.BadRequestException;
+import org.apache.gravitino.server.web.filter.RequestValidation;
 import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
 
 /**
@@ -57,7 +59,7 @@ public class AssociateTagAuthorizationExecutor extends 
CommonAuthorizerExecutor
   public boolean execute(AuthorizationRequestContext context) throws Exception 
{
     Object request = extractFromParameters(parameters, args);
     if (request == null) {
-      return false;
+      throw new BadRequestException("Request body cannot be null");
     }
 
     context.setOriginalAuthorizationExpression(expression);
@@ -67,7 +69,7 @@ public class AssociateTagAuthorizationExecutor extends 
CommonAuthorizerExecutor
         request instanceof TagsAssociateRequest,
         "Only tag can use AssociateTagAuthorizationExecutor, please contact 
the administrator.");
     TagsAssociateRequest tagsAssociateRequest = (TagsAssociateRequest) request;
-    tagsAssociateRequest.validate();
+    RequestValidation.validate(tagsAssociateRequest::validate);
     // Authorize both 'tagsToAdd' and 'tagsToRemove' fields.
     return authorizeTag(tagsAssociateRequest.getTagsToAdd(), context, 
targetType)
         && authorizeTag(tagsAssociateRequest.getTagsToRemove(), context, 
targetType);
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LineageAuthorizationExecutor.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LineageAuthorizationExecutor.java
index 5af3a60d62..2ef2dafb9e 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LineageAuthorizationExecutor.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/LineageAuthorizationExecutor.java
@@ -38,9 +38,9 @@ import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.MetadataObjects;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AuthorizationRequestContext;
-import org.apache.gravitino.exceptions.BadRequestException;
 import org.apache.gravitino.lineage.source.rest.LineageEventValidator;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
+import org.apache.gravitino.server.web.filter.RequestValidation;
 import org.apache.gravitino.utils.MetadataObjectUtil;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 
@@ -80,11 +80,7 @@ public class LineageAuthorizationExecutor implements 
AuthorizationExecutor {
   @Override
   public boolean execute(AuthorizationRequestContext context) {
     if (!authorizationTargetsResolved) {
-      try {
-        resolveAuthorizationTargets(extractRunEvent());
-      } catch (IllegalArgumentException e) {
-        throw new BadRequestException(e, "%s", e.getMessage());
-      }
+      RequestValidation.validate(() -> 
resolveAuthorizationTargets(extractRunEvent()));
     }
 
     AuthorizationExpressionEvaluator evaluator = new 
AuthorizationExpressionEvaluator(expression);
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 36b49e43e1..d94febf8dc 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
@@ -26,9 +26,11 @@ import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.exceptions.BadRequestException;
 import 
org.apache.gravitino.server.authorization.annotations.ExpressionCondition;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
 import org.apache.gravitino.server.web.filter.ParameterUtil;
+import org.apache.gravitino.server.web.filter.RequestValidation;
 
 /**
  * Authorization executor for load table operations.
@@ -107,7 +109,12 @@ public class LoadTableAuthorizationExecutor extends 
CommonAuthorizerExecutor {
     return Arrays.stream(privileges.split(","))
         .map(String::trim)
         .filter(StringUtils::isNotBlank)
-        .map(Privilege.Name::valueOf)
-        .anyMatch(Privilege.Name.MODIFY_TABLE::equals);
+        .map(
+            privilege ->
+                RequestValidation.parse(
+                    () -> Privilege.Name.valueOf(privilege),
+                    e -> new BadRequestException(e, "Invalid privilege: %s", 
privilege)))
+        .toList()
+        .contains(Privilege.Name.MODIFY_TABLE);
   }
 }
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
index aaadc129b9..dec8e1eb4e 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/filter/authorization/RunJobAuthorizationExecutor.java
@@ -30,6 +30,7 @@ import 
org.apache.gravitino.authorization.AuthorizationRequestContext;
 import org.apache.gravitino.dto.requests.JobRunRequest;
 import org.apache.gravitino.exceptions.BadRequestException;
 import 
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionEvaluator;
+import org.apache.gravitino.server.web.filter.RequestValidation;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 
 public class RunJobAuthorizationExecutor implements AuthorizationExecutor {
@@ -71,13 +72,7 @@ public class RunJobAuthorizationExecutor implements 
AuthorizationExecutor {
         "Expected JobRunRequest but found %s",
         request.getClass().getSimpleName());
     JobRunRequest jobRunRequest = (JobRunRequest) request;
-    try {
-      jobRunRequest.validate();
-    } catch (IllegalArgumentException e) {
-      // validate() throws IllegalArgumentException; map to BadRequest so the 
interceptor returns
-      // 400 instead of the catch-all 500 authorization internal error.
-      throw new BadRequestException(e, "%s", e.getMessage());
-    }
+    RequestValidation.validate(jobRunRequest::validate);
     String jobTemplateName = jobRunRequest.getJobTemplateName();
     NameIdentifier metalake = metadataContext.get(Entity.EntityType.METALAKE);
     NameIdentifier jobTemplateNameIdentifier =
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 33ffa50053..63175f93cc 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
@@ -28,6 +28,7 @@ import static org.mockito.Mockito.mockStatic;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableList;
@@ -58,6 +59,8 @@ import org.apache.gravitino.catalog.TableDispatcher;
 import org.apache.gravitino.catalog.ViewDispatcher;
 import org.apache.gravitino.dto.requests.CatalogUpdateRequest;
 import org.apache.gravitino.dto.requests.CatalogUpdatesRequest;
+import org.apache.gravitino.dto.requests.JobRunRequest;
+import org.apache.gravitino.dto.requests.PoliciesAssociateRequest;
 import org.apache.gravitino.dto.requests.SchemaCreateRequest;
 import org.apache.gravitino.dto.requests.TagsAssociateRequest;
 import org.apache.gravitino.dto.responses.ErrorConstants;
@@ -66,6 +69,7 @@ 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.json.JsonUtils;
 import org.apache.gravitino.listener.EventBus;
 import 
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
 import org.apache.gravitino.meta.AuditInfo;
@@ -87,6 +91,9 @@ import 
org.apache.gravitino.server.authorization.annotations.AuthorizationObject
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationRequest;
 import org.apache.gravitino.server.web.Utils;
 import org.apache.gravitino.server.web.rest.CatalogOperations;
+import org.apache.gravitino.server.web.rest.JobOperations;
+import org.apache.gravitino.server.web.rest.MetadataObjectPolicyOperations;
+import org.apache.gravitino.server.web.rest.MetadataObjectTagOperations;
 import org.apache.gravitino.server.web.rest.SchemaOperations;
 import org.apache.gravitino.server.web.rest.TableOperations;
 import org.apache.gravitino.server.web.rest.ViewOperations;
@@ -96,6 +103,8 @@ import org.glassfish.hk2.api.Descriptor;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.mockito.ArgumentCaptor;
 import org.mockito.ArgumentMatchers;
 import org.mockito.MockedStatic;
@@ -109,6 +118,158 @@ public class TestGravitinoInterceptionService {
     RequestContext.clear();
   }
 
+  /** Verifies invalid tag and policy association bodies are rejected before 
resource dispatch. */
+  @ParameterizedTest
+  @ValueSource(strings = {"tags", "policies"})
+  public void testInvalidAssociationBodyReturnsBadRequest(String kind) throws 
Throwable {
+    boolean tags = kind.equals("tags");
+    Method method =
+        tags
+            ? MetadataObjectTagOperations.class.getMethod(
+                "associateTagsForObject",
+                String.class,
+                String.class,
+                String.class,
+                TagsAssociateRequest.class)
+            : MetadataObjectPolicyOperations.class.getMethod(
+                "associatePoliciesForObject",
+                String.class,
+                String.class,
+                String.class,
+                PoliciesAssociateRequest.class);
+    Class<?> requestClass = tags ? TagsAssociateRequest.class : 
PoliciesAssociateRequest.class;
+    try (MockedStatic<PrincipalUtils> principals = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> providers =
+            mockStatic(GravitinoAuthorizerProvider.class);
+        MockedStatic<AuthorizationUtils> authUtils = 
mockStatic(AuthorizationUtils.class)) {
+      principals.when(PrincipalUtils::getCurrentPrincipal).thenReturn(new 
UserPrincipal("tester"));
+      principals.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+      GravitinoAuthorizerProvider provider = 
mock(GravitinoAuthorizerProvider.class);
+      
providers.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+      GravitinoAuthorizer authorizer = mock(GravitinoAuthorizer.class);
+      when(provider.getGravitinoAuthorizer()).thenReturn(authorizer);
+      String add = kind + "ToAdd";
+      String remove = kind + "ToRemove";
+      String bothNull = add + " and " + remove + " cannot both be null";
+      String[][] cases = {
+        {"{}", bothNull},
+        {"{\"" + add + "\":null,\"" + remove + "\":null}", bothNull},
+        {"null", "Request body cannot be null"},
+        {"{\"" + add + "\":[null]}", add},
+        {"{\"" + add + "\":[\"\"]}", add},
+        {"{\"" + add + "\":[\" \"]}", add},
+        {"{\"" + remove + "\":[null]}", remove},
+        {"{\"" + remove + "\":[\"\"]}", remove},
+        {"{\"" + remove + "\":[\" \"]}", remove}
+      };
+      for (String[] testCase : cases) {
+        Object request = JsonUtils.objectMapper().readValue(testCase[0], 
requestClass);
+        MethodInvocation invocation = mock(MethodInvocation.class);
+        when(invocation.getMethod()).thenReturn(method);
+        when(invocation.getArguments())
+            .thenReturn(new Object[] {"metalake", "schema", "catalog.schema", 
request});
+        Response response =
+            (Response)
+                new GravitinoInterceptionService()
+                    .getMethodInterceptors(method)
+                    .get(0)
+                    .invoke(invocation);
+        assertEquals(400, response.getStatus(), testCase[0]);
+        ErrorResponse error = (ErrorResponse) response.getEntity();
+        assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, error.getCode());
+        Assertions.assertTrue(error.getMessage().contains(testCase[1]), 
error.getMessage());
+        Assertions.assertFalse(error.getMessage().contains("Authorization 
failed"));
+        verify(invocation, never()).proceed();
+      }
+      verifyNoInteractions(authorizer);
+    }
+  }
+
+  /** Verifies that malformed object names return client errors for tag and 
policy endpoints. */
+  @Test
+  public void testInvalidMetadataObjectFullNameReturnsBadRequest() throws 
Throwable {
+    Method tags =
+        MetadataObjectTagOperations.class.getMethod(
+            "associateTagsForObject",
+            String.class,
+            String.class,
+            String.class,
+            TagsAssociateRequest.class);
+    Method policies =
+        MetadataObjectPolicyOperations.class.getMethod(
+            "listPoliciesForMetadataObject",
+            String.class,
+            String.class,
+            String.class,
+            boolean.class);
+    for (Method method : new Method[] {tags, policies}) {
+      for (String fullName : new String[] {"catalog", "catalog.schema.extra", 
"catalog.*", " "}) {
+        MethodInvocation invocation = mock(MethodInvocation.class);
+        when(invocation.getMethod()).thenReturn(method);
+        Object request =
+            method.equals(tags) ? new TagsAssociateRequest(new String[] 
{"tag"}, null) : false;
+        when(invocation.getArguments())
+            .thenReturn(new Object[] {"metalake", "schema", fullName, 
request});
+        Response response =
+            (Response)
+                new GravitinoInterceptionService()
+                    .getMethodInterceptors(method)
+                    .get(0)
+                    .invoke(invocation);
+        assertEquals(400, response.getStatus(), fullName);
+        ErrorResponse error = (ErrorResponse) response.getEntity();
+        assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, error.getCode());
+        assertEquals(IllegalArgumentException.class.getSimpleName(), 
error.getType());
+        Assertions.assertFalse(error.getMessage().contains("Authorization 
failed"));
+        verify(invocation, never()).proceed();
+      }
+    }
+  }
+
+  /** Verifies that invalid privileges return 400 even after a valid 
MODIFY_TABLE privilege. */
+  @Test
+  public void testInvalidLoadTablePrivilegesReturnsBadRequest() throws 
Throwable {
+    try (MockedStatic<PrincipalUtils> principals = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> providers =
+            mockStatic(GravitinoAuthorizerProvider.class);
+        MockedStatic<AuthorizationUtils> authUtils = 
mockStatic(AuthorizationUtils.class)) {
+      principals.when(PrincipalUtils::getCurrentPrincipal).thenReturn(new 
UserPrincipal("tester"));
+      principals.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+      GravitinoAuthorizerProvider provider = 
mock(GravitinoAuthorizerProvider.class);
+      
providers.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+      
when(provider.getGravitinoAuthorizer()).thenReturn(mock(GravitinoAuthorizer.class));
+      Method method =
+          TableOperations.class.getMethod(
+              "loadTable", String.class, String.class, String.class, 
String.class, String.class);
+      for (String privileges :
+          new String[] {
+            "BOGUS",
+            "select_table",
+            "SELECT_TABLE,BOGUS",
+            "MODIFY_TABLE,BOGUS",
+            "BOGUS,MODIFY_TABLE"
+          }) {
+        MethodInvocation invocation = mock(MethodInvocation.class);
+        when(invocation.getMethod()).thenReturn(method);
+        when(invocation.getArguments())
+            .thenReturn(new Object[] {"metalake", "catalog", "schema", 
"table", privileges});
+        Response response =
+            (Response)
+                new GravitinoInterceptionService()
+                    .getMethodInterceptors(method)
+                    .get(0)
+                    .invoke(invocation);
+        assertEquals(400, response.getStatus(), privileges);
+        ErrorResponse error = (ErrorResponse) response.getEntity();
+        assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, error.getCode());
+        assertEquals(
+            "Invalid privilege: " + (privileges.equals("select_table") ? 
"select_table" : "BOGUS"),
+            error.getMessage());
+        verify(invocation, never()).proceed();
+      }
+    }
+  }
+
   @Test
   public void testPathMetalakeValidationPrecedesExecutorConstruction() throws 
Throwable {
     Method method =
@@ -355,6 +516,45 @@ public class TestGravitinoInterceptionService {
     }
   }
 
+  /** Verifies that authorizer failures after successful input validation 
remain server errors. */
+  @Test
+  public void testAuthorizerIllegalArgumentExceptionRemainsInternalError() 
throws Throwable {
+    try (MockedStatic<PrincipalUtils> principals = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> providers =
+            mockStatic(GravitinoAuthorizerProvider.class);
+        MockedStatic<AuthorizationUtils> authUtils = 
mockStatic(AuthorizationUtils.class)) {
+      principals.when(PrincipalUtils::getCurrentPrincipal).thenReturn(new 
UserPrincipal("tester"));
+      principals.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+      GravitinoAuthorizerProvider provider = 
mock(GravitinoAuthorizerProvider.class);
+      GravitinoAuthorizer authorizer = mock(GravitinoAuthorizer.class);
+      
providers.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+      when(provider.getGravitinoAuthorizer()).thenReturn(authorizer);
+      when(authorizer.isOwner(any(), any(), any(), any()))
+          .thenThrow(new IllegalArgumentException("Invalid authorizer 
policy"));
+      Method method = JobOperations.class.getMethod("runJob", String.class, 
JobRunRequest.class);
+      MethodInvocation invocation = mock(MethodInvocation.class);
+      when(invocation.getMethod()).thenReturn(method);
+      when(invocation.getArguments())
+          .thenReturn(
+              new Object[] {"metalake", new JobRunRequest("template", 
Collections.emptyMap())});
+
+      Response response =
+          (Response)
+              new GravitinoInterceptionService()
+                  .getMethodInterceptors(method)
+                  .get(0)
+                  .invoke(invocation);
+
+      assertEquals(500, response.getStatus());
+      Assertions.assertTrue(
+          ((ErrorResponse) response.getEntity())
+              .getMessage()
+              .contains("Authorization failed due to system internal error"));
+      verify(authorizer).isOwner(any(), any(), any(), any());
+      verify(invocation, never()).proceed();
+    }
+  }
+
   @Test
   public void testUnexpectedIllegalArgumentExceptionRemainsInternalError() 
throws Throwable {
     try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);

Reply via email to