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

yuqi1129 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 79527d7669 [#10166] fix(server): reject null request bodies in bulk 
role and run-job operations (#13670)
79527d7669 is described below

commit 79527d7669f0f25714d11e302c9e8c8a6cda5234
Author: dennismdejong <[email protected]>
AuthorDate: Fri Oct 9 15:10:32 2026 +0200

    [#10166] fix(server): reject null request bodies in bulk role and run-job 
operations (#13670)
    
    ### What changes were proposed in this pull request?
    
    Guard the REST operations that could answer a null request body with
    HTTP 500 through an unintended NullPointerException:
    
    - `BulkOperations.addRoles` / `removeRoles`: reject a null body the same
    way as the sibling bulk user/group operations (`handleRoleException`,
    HTTP 400).
    - `JobOperations.runJob`: guard the null body before the log statement
    that dereferences the request.
    - `CatalogOperations.setCatalog` / `MetalakeOperations.setMetalake`:
    behavior-preserving refactor — compute the operation type once and reuse
    it in the catch blocks and log statements instead of re-reading the
    request. These two already reject null bodies at the top of the handler,
    so this is not an additional null-body fix.
    
    Note: the permission endpoints named in the issue (`revokeRolesFromUser`
    and siblings) are already guarded on main (handled by
    https://github.com/apache/gravitino/pull/12590 /
    https://github.com/apache/gravitino/pull/12866), so no change is needed
    there; this PR completes the same pattern for the operations that were
    still missing it.
    
    ### Why are the changes needed?
    
    An omitted, empty, or JSON `null` request body is a client error, but
    these endpoints answered it with HTTP 500 through an unintended
    NullPointerException (in the handler, or before the `try` block for
    `runJob`), masking the original failure.
    
    Caveat on the `runJob` guarantee: the HTTP 400 applies only when the
    resource method is reached. With authorization enabled, a null request
    is rejected with HTTP 403 by the authorization filter before the handler
    (pre-existing behavior, unchanged by this PR).
    
    Fix: https://github.com/apache/gravitino/issues/10166
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes, for `BulkOperations.addRoles`/`removeRoles` a null body now returns
    HTTP 400 with error code `1001` (`IllegalArgumentException`, message
    "Request body cannot be null") instead of HTTP 500. For
    `JobOperations.runJob`, the same HTTP 400 is returned when the resource
    method is reached; with authorization enabled the existing filter
    rejects a null body with HTTP 403 first (pre-existing). The
    `setCatalog`/`setMetalake` changes are behavior-preserving refactors.
    Valid requests are unchanged.
    
    ### How was this patch tested?
    
    - New regression tests (`testBulkAddRolesWithNullRequest`,
    `testBulkRemoveRolesWithNullRequest`, `testRunJobWithNullRequest`) fail
    before the change (expected 400, got 500) and pass after.
    - `JAVA_HOME=/opt/homebrew/opt/openjdk@17 ./gradlew :server:test
    -PskipITs`: 64 test classes, 559 tests, 0 failures.
    - `./gradlew :server:spotlessCheck` passes.
---
 .../gravitino/server/web/rest/BulkOperations.java  | 16 ++++++++++++++++
 .../server/web/rest/CatalogOperations.java         |  4 ++--
 .../gravitino/server/web/rest/JobOperations.java   |  8 ++++++++
 .../server/web/rest/MetalakeOperations.java        | 14 +++++++++-----
 .../server/web/rest/TestBulkOperations.java        | 22 ++++++++++++++++++++++
 .../server/web/rest/TestJobOperations.java         | 16 ++++++++++++++++
 6 files changed, 73 insertions(+), 7 deletions(-)

diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java
index 23e0bda28e..a73dd907d2 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/BulkOperations.java
@@ -355,6 +355,14 @@ public class BulkOperations {
       @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
           String metalake,
       BulkRoleAddRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleRoleException(
+          OperationType.CREATE,
+          "",
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
       return Utils.doAs(
           httpRequest,
@@ -417,6 +425,14 @@ public class BulkOperations {
       @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
           String metalake,
       BulkRemoveRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleRoleException(
+          OperationType.DELETE,
+          "",
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     try {
       return Utils.doAs(
           httpRequest,
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
index 09679b7515..d47dc71b30 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/CatalogOperations.java
@@ -313,7 +313,7 @@ public class CatalogOperations {
             Response response = Utils.ok(new BaseResponse());
             LOG.info(
                 "Successfully {} catalog: {}.{}",
-                request.isInUse() ? "enable" : "disable",
+                op == OperationType.ENABLE ? "enable" : "disable",
                 metalake,
                 catalogName);
             return response;
@@ -322,7 +322,7 @@ public class CatalogOperations {
     } catch (Exception e) {
       LOG.info(
           "Failed to {} catalog: {}.{}",
-          request.isInUse() ? "enable" : "disable",
+          op == OperationType.ENABLE ? "enable" : "disable",
           metalake,
           catalogName);
       return ExceptionHandlers.handleCatalogException(op, catalogName, 
metalake, e);
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
index 863e619d4c..4a185ab4f6 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/JobOperations.java
@@ -416,6 +416,14 @@ public class JobOperations {
           String metalake,
       @AuthorizationRequest(type = AuthorizationRequest.RequestType.RUN_JOB)
           JobRunRequest request) {
+    if (request == null) {
+      return ExceptionHandlers.handleJobException(
+          OperationType.RUN,
+          "",
+          metalake,
+          new IllegalArgumentException("Request body cannot be null"));
+    }
+
     LOG.info(
         "Received request to run job {} in metalake: {}", 
request.getJobTemplateName(), metalake);
 
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
index 5e6dd53b9b..c577bd3298 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/MetalakeOperations.java
@@ -189,12 +189,14 @@ public class MetalakeOperations {
     }
 
     LOG.info("Received set request for metalake: {}", metalakeName);
+    OperationType op = request.isInUse() ? OperationType.ENABLE : 
OperationType.DISABLE;
+
     try {
       return Utils.doAs(
           httpRequest,
           () -> {
             NameIdentifier identifier = 
NameIdentifierUtil.ofMetalake(metalakeName);
-            if (request.isInUse()) {
+            if (op == OperationType.ENABLE) {
               metalakeDispatcher.enableMetalake(identifier);
             } else {
               metalakeDispatcher.disableMetalake(identifier);
@@ -202,15 +204,17 @@ public class MetalakeOperations {
             Response response = Utils.ok(new BaseResponse());
             LOG.info(
                 "Successfully {} metalake: {}",
-                request.isInUse() ? "enable" : "disable",
+                op == OperationType.ENABLE ? "enable" : "disable",
                 metalakeName);
             return response;
           });
 
     } catch (Exception e) {
-      LOG.info("Failed to {} metalake: {}", request.isInUse() ? "enable" : 
"disable", metalakeName);
-      return ExceptionHandlers.handleMetalakeException(
-          request.isInUse() ? OperationType.ENABLE : OperationType.DISABLE, 
metalakeName, e);
+      LOG.info(
+          "Failed to {} metalake: {}",
+          op == OperationType.ENABLE ? "enable" : "disable",
+          metalakeName);
+      return ExceptionHandlers.handleMetalakeException(op, metalakeName, e);
     }
   }
 
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
index 30458b689d..718e6ef6e2 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestBulkOperations.java
@@ -401,6 +401,28 @@ public class TestBulkOperations extends BaseOperationsTest 
{
     assertNullRequestBodyRejected(response);
   }
 
+  @Test
+  public void testBulkAddRolesWithNullRequest() {
+    Response response =
+        target("/bulk/metalakes/metalake1/roles/add")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(new byte[0], MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(response);
+  }
+
+  @Test
+  public void testBulkRemoveRolesWithNullRequest() {
+    Response response =
+        target("/bulk/metalakes/metalake1/roles/remove")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity("null", MediaType.APPLICATION_JSON_TYPE));
+
+    assertNullRequestBodyRejected(response);
+  }
+
   @Test
   public void testBulkRejectsEmptyAndExceededRequest() {
     Response emptyResponse =
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
index 5714ec0d20..da19b642fa 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestJobOperations.java
@@ -263,6 +263,22 @@ public class TestJobOperations extends JerseyTest {
     Assertions.assertTrue(errorResponse.getMessage().contains("Request body 
cannot be null"));
   }
 
+  @Test
+  public void testRunJobWithNullRequest() {
+    Response resp =
+        target(jobRunPath())
+            .request(APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .post(Entity.entity(new byte[0], APPLICATION_JSON_TYPE));
+
+    Assertions.assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), 
resp.getStatus());
+    ErrorResponse errorResponse = resp.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, 
errorResponse.getCode());
+    Assertions.assertEquals(
+        IllegalArgumentException.class.getSimpleName(), 
errorResponse.getType());
+    Assertions.assertTrue(errorResponse.getMessage().contains("Request body 
cannot be null"));
+  }
+
   @Test
   public void testRegisterJobTemplate() {
     JobTemplateEntity template =

Reply via email to