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 a74070af5a [#14610] fix(authz): Return 400 for invalid request 
parameters (#14612)
a74070af5a is described below

commit a74070af5ad36dfdeae209dec621d996b0c51aff
Author: Jerry Shao <[email protected]>
AuthorDate: Thu Nov 5 09:40:53 2026 +0800

    [#14610] fix(authz): Return 400 for invalid request parameters (#14612)
    
    ### What changes were proposed in this pull request?
    
    Return HTTP 400 for invalid metadata object names and load-table
    privileges encountered during authorization.
    
    Validate all requested privileges, including invalid values following
    `MODIFY_TABLE`. Preserve HTTP 500 for unexpected internal errors. Add
    interceptor regression coverage for malformed job-run requests using the
    existing upstream fix.
    
    ### Why are the changes needed?
    
    Client validation errors can be wrapped as authorization internal errors
    and incorrectly return HTTP 500.
    
    Fixes #14610
    
    This PR targets main; tag/policy association validation on branch-1.3
    requires a separate fix.
    
    ### Does this PR introduce _any_ user-facing change?
    
    Invalid object names and privileges return HTTP 400 with the validation
    error instead of HTTP 500. Unknown privileges are consistently rejected
    regardless of their position in the list.
    
    ### How was this patch tested?
    
    - Added regression tests for invalid object names on tag/policy
    endpoints, invalid privileges, malformed job-run requests, and empty tag
    association bodies.
    - Passed 139 targeted authorization and REST tests after rebasing onto
    the latest main.
    - Independent review passed 65 targeted tests before the rebase.
    - Ran `./gradlew spotlessApply`, `:server:spotlessCheck`, and `git diff
    --check`.
---
 .../web/filter/GravitinoInterceptionService.java   |   6 +-
 .../gravitino/server/web/filter/ParameterUtil.java |   7 +-
 .../server/web/filter/RequestValidation.java       |  75 +++++++
 .../LineageAuthorizationExecutor.java              |   8 +-
 .../LoadTableAuthorizationExecutor.java            |  11 +-
 .../authorization/RunJobAuthorizationExecutor.java |   9 +-
 .../filter/TestGravitinoInterceptionService.java   | 223 +++++++++++++++++++++
 7 files changed, 319 insertions(+), 20 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 3af44ce85b..92557aaf59 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
@@ -270,9 +270,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();
@@ -295,6 +292,9 @@ public class GravitinoInterceptionService implements 
InterceptionService {
           scope.bindIfRead(method, metalakeIdent, authorizationRequestContext);
         }
         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/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 ba52025fa3..ad16df5a94 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
@@ -62,10 +62,12 @@ 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.SchemaCreateRequest;
 import org.apache.gravitino.dto.requests.SemanticModelCreateRequest;
 import org.apache.gravitino.dto.requests.SemanticModelUpdatesRequest;
 import org.apache.gravitino.dto.requests.TagValuesAssociateRequest;
+import org.apache.gravitino.dto.requests.TagsAssociateRequest;
 import org.apache.gravitino.dto.responses.ErrorConstants;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.exceptions.ForbiddenException;
@@ -95,6 +97,8 @@ 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.SecretsProviderOperations;
@@ -602,6 +606,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);
@@ -775,6 +818,186 @@ public class TestGravitinoInterceptionService {
     }
   }
 
+  /** 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();
+      }
+    }
+  }
+
+  /** Verifies that invalid job bodies produce client errors before the 
resource is invoked. */
+  @Test
+  public void testInvalidJobRunBodyReturnsBadRequest() throws Throwable {
+    try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+            mockStatic(GravitinoAuthorizerProvider.class);
+        MockedStatic<AuthorizationUtils> authUtilsMocked = 
mockStatic(AuthorizationUtils.class)) {
+      principalUtilsMocked
+          .when(PrincipalUtils::getCurrentPrincipal)
+          .thenReturn(new UserPrincipal("tester"));
+      
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+      GravitinoAuthorizerProvider provider = 
mock(GravitinoAuthorizerProvider.class);
+      
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+      
when(provider.getGravitinoAuthorizer()).thenReturn(mock(GravitinoAuthorizer.class));
+
+      Method method = JobOperations.class.getMethod("runJob", String.class, 
JobRunRequest.class);
+      MethodInterceptor interceptor =
+          new 
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+      for (String body :
+          new String[] {
+            "{}",
+            "{\"jobConf\":{}}",
+            "{\"jobTemplateName\":null}",
+            "{\"jobTemplateName\":\"\"}",
+            "{\"jobTemplateName\":\" \"}"
+          }) {
+        JobRunRequest request = JsonUtils.objectMapper().readValue(body, 
JobRunRequest.class);
+        MethodInvocation invocation = mock(MethodInvocation.class);
+        when(invocation.getMethod()).thenReturn(method);
+        when(invocation.getArguments()).thenReturn(new Object[] 
{"testMetalake", request});
+
+        Response response = (Response) interceptor.invoke(invocation);
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
response.getStatus(), body);
+        ErrorResponse error = (ErrorResponse) response.getEntity();
+        assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, error.getCode());
+        assertEquals("\"jobTemplateName\" is required and cannot be empty", 
error.getMessage());
+        verify(invocation, never()).proceed();
+      }
+    }
+  }
+
+  /** Verifies that empty V1 tag association bodies remain client errors with 
authorization. */
+  @Test
+  public void testEmptyTagAssociationBodyReturnsBadRequest() throws Throwable {
+    try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
+        MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+            mockStatic(GravitinoAuthorizerProvider.class);
+        MockedStatic<AuthorizationUtils> authUtilsMocked = 
mockStatic(AuthorizationUtils.class)) {
+      principalUtilsMocked
+          .when(PrincipalUtils::getCurrentPrincipal)
+          .thenReturn(new UserPrincipal("tester"));
+      
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+      principalUtilsMocked.when(() -> PrincipalUtils.doAs(any(), 
any())).thenCallRealMethod();
+      GravitinoAuthorizerProvider provider = 
mock(GravitinoAuthorizerProvider.class);
+      
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+      
when(provider.getGravitinoAuthorizer()).thenReturn(mock(GravitinoAuthorizer.class));
+      TagDispatcher dispatcher = mock(TagDispatcher.class);
+      MetadataObjectTagOperations operations = new 
MetadataObjectTagOperations(dispatcher);
+      FieldUtils.writeField(operations, "httpRequest", 
mock(HttpServletRequest.class), true);
+      Method method =
+          MetadataObjectTagOperations.class.getMethod(
+              "associateTagsForObject",
+              String.class,
+              String.class,
+              String.class,
+              TagsAssociateRequest.class);
+      MethodInterceptor interceptor =
+          new 
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+      for (String body : new String[] {"{}", 
"{\"tagsToAdd\":null,\"tagsToRemove\":null}"}) {
+        TagsAssociateRequest request =
+            JsonUtils.objectMapper().readValue(body, 
TagsAssociateRequest.class);
+        MethodInvocation invocation = mock(MethodInvocation.class);
+        when(invocation.getMethod()).thenReturn(method);
+        when(invocation.getArguments())
+            .thenReturn(new Object[] {"testMetalake", "schema", 
"catalog.schema", request});
+        when(invocation.proceed())
+            .thenAnswer(
+                ignored ->
+                    operations.associateTagsForObject(
+                        "testMetalake", "schema", "catalog.schema", request));
+
+        Response response = (Response) interceptor.invoke(invocation);
+
+        assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
response.getStatus(), body);
+        ErrorResponse error = (ErrorResponse) response.getEntity();
+        assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, error.getCode());
+        Assertions.assertTrue(
+            error.getMessage().contains("tagsToAdd and tagsToRemove cannot 
both be null"));
+        verify(invocation).proceed();
+      }
+      verify(dispatcher, never()).associateTagsForMetadataObject(any(), any(), 
any(), any());
+    }
+  }
+
   @Test
   public void 
testInvalidV2TagAssociationBodyReturnsBadRequestAfterAuthorization()
       throws Throwable {

Reply via email to