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

jerryshao pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new b91f65c0e9 [Cherry-pick to branch-1.3] [#14571][#14579] docs/fix(idp): 
Document 401/406; reject null IdP bodies (#14572) (#14591)
b91f65c0e9 is described below

commit b91f65c0e94e44183432ccfc90c453edd2c14a6c
Author: MaSai <[email protected]>
AuthorDate: Wed Nov 4 09:29:44 2026 +0800

    [Cherry-pick to branch-1.3] [#14571][#14579] docs/fix(idp): Document 
401/406; reject null IdP bodies (#14572) (#14591)
    
    ### What changes were proposed in this pull request?
    
    Manual conflict-free backport of #14572 / `58ef9fd6ff` onto
    `branch-1.3`:
    
    - Document **401** and **406** on the built-in IdP operations that exist
    on `branch-1.3`.
    - Narrow `IdpForbiddenErrorResponse` so it does not claim
    unauthenticated callers receive 403; align the Forbidden example with
    `SERVICE_ADMIN_REQUIRED_MESSAGE`.
    - Return **400** (`Request body cannot be null`) for null JSON bodies on
    IdP user/group write APIs instead of NPE **500**.
    
    `branch-1.3` does not include `PUT /idp/users/{user}/enabled`, so that
    operation from main is intentionally omitted (that is what conflicted in
    #14590).
    
    ### Why are the changes needed?
    
    Fixes #14571 and #14579 on the 1.3 line. Supersedes conflicted bot
    cherry-pick #14590.
    
    ### Does this PR introduce _any_ user-facing change?
    
    - Docs / OpenAPI contract updates for 401/406/Forbidden wording.
    - IdP write APIs now return 400 instead of 500 when the request body is
    null.
    
    ### How was this patch tested?
    
    - `./gradlew :plugins:idp-basic:test --tests
    org.apache.gravitino.idp.web.rest.TestIdpOperations` (includes
    `testNullRequestBodyReturnsBadRequest`)
    
    Co-authored-by: Cursor <[email protected]>
---
 docs/open-api/idp/idp.yaml                         | 75 +++++++++++++++++++++-
 .../org/apache/gravitino/idp/web/IdpRESTUtils.java | 17 +++++
 .../gravitino/idp/web/rest/IdpGroupOperations.java |  8 +++
 .../gravitino/idp/web/rest/IdpUserOperations.java  |  8 +++
 .../gravitino/idp/web/rest/TestIdpOperations.java  | 23 +++++++
 5 files changed, 129 insertions(+), 2 deletions(-)

diff --git a/docs/open-api/idp/idp.yaml b/docs/open-api/idp/idp.yaml
index 74e64a395b..c84d61149d 100644
--- a/docs/open-api/idp/idp.yaml
+++ b/docs/open-api/idp/idp.yaml
@@ -67,8 +67,12 @@ paths:
               examples:
                 AlreadyExistsException:
                   $ref: "#/components/examples/IdpAlreadyExistsException"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "5xx":
           $ref: "../openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -92,8 +96,12 @@ paths:
               examples:
                 IdpUserGetResponse:
                   $ref: "#/components/examples/IdpUserGetResponse"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "404":
           description: Not Found - The specified local user store user does 
not exist
           content:
@@ -115,8 +123,12 @@ paths:
       responses:
         "200":
           $ref: "../openapi.yaml#/components/responses/RemoveResponse"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "5xx":
           $ref: "../openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -165,8 +177,12 @@ paths:
               examples:
                 IllegalArgumentException:
                   $ref: 
"#/components/examples/IdpChangePasswordIllegalArgumentException"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "404":
           description: Not Found - The specified local user store user does 
not exist
           content:
@@ -223,8 +239,12 @@ paths:
               examples:
                 AlreadyExistsException:
                   $ref: "#/components/examples/IdpGroupAlreadyExistsException"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "5xx":
           $ref: "../openapi.yaml#/components/responses/ServerErrorResponse"
 
@@ -248,8 +268,12 @@ paths:
               examples:
                 IdpGroupGetResponse:
                   $ref: "#/components/examples/IdpGroupGetResponse"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "404":
           description: Not Found - The specified local user store group does 
not exist
           content:
@@ -273,8 +297,12 @@ paths:
       responses:
         "200":
           $ref: "../openapi.yaml#/components/responses/RemoveResponse"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "409":
           description: Conflict - The group is not empty and force is false
           content:
@@ -325,8 +353,12 @@ paths:
               examples:
                 IllegalArgumentException:
                   $ref: 
"#/components/examples/IdpGroupMembershipIllegalArgumentException"
+        "401":
+          $ref: "#/components/responses/IdpUnauthorizedErrorResponse"
         "403":
           $ref: "#/components/responses/IdpForbiddenErrorResponse"
+        "406":
+          $ref: "#/components/responses/IdpNotAcceptableErrorResponse"
         "404":
           description: Not Found - The specified local user store group or 
user does not exist
           content:
@@ -476,12 +508,23 @@ components:
           $ref: "#/components/schemas/IdpGroup"
 
   responses:
+    IdpUnauthorizedErrorResponse:
+      description: Unauthorized - Authentication credentials are missing or 
invalid
+      content:
+        application/vnd.gravitino.v1+json:
+          schema:
+            $ref: "../openapi.yaml#/components/schemas/ErrorModel"
+          examples:
+            UnauthorizedException:
+              $ref: "#/components/examples/IdpUnauthorizedException"
+
     IdpForbiddenErrorResponse:
       description:
         Forbidden - Callers listed in `gravitino.authorization.serviceAdmins` 
may manage local
         user store users and groups. Authenticated non-admin users may only 
`GET`
         `/idp/users/{user}` or `PUT` `/idp/users/{user}/password` for their 
own username.
-        Unauthenticated requests and other non-admin IdP management calls 
receive this response.
+        Other authenticated non-admin IdP management calls receive this 
response. Missing or
+        invalid credentials return 401 instead.
       content:
         application/vnd.gravitino.v1+json:
           schema:
@@ -490,6 +533,20 @@ components:
             ForbiddenException:
               $ref: "#/components/examples/IdpForbiddenException"
 
+    IdpNotAcceptableErrorResponse:
+      description: >
+        Not Acceptable - Returned when `Accept` (or a versioned 
`Content-Type`) declares an
+        unsupported Gravitino API version, for example 
`application/vnd.gravitino.v99+json`.
+        Missing Accept, `*/*`, or `application/json` alone are rewritten to 
the default
+        `application/vnd.gravitino.v1+json` and do not produce this response.
+      content:
+        application/vnd.gravitino.v1+json:
+          schema:
+            $ref: "../openapi.yaml#/components/schemas/ErrorModel"
+          examples:
+            UnsupportedApiVersion:
+              $ref: "#/components/examples/IdpUnsupportedApiVersionException"
+
   examples:
     AddUserRequest:
       value: {
@@ -657,9 +714,23 @@ components:
         "message": "Failed to operate local user store group [engineers] 
operation [REMOVE], reason [IdP group engineers is not empty, use force=true to 
delete it]"
       }
 
+    IdpUnauthorizedException:
+      value: {
+        "code": 1011,
+        "type": "UnauthorizedException",
+        "message": "The provided credentials did not support"
+      }
+
     IdpForbiddenException:
       value: {
         "code": 1008,
         "type": "ForbiddenException",
-        "message": "Only service admins can manage local user store users and 
groups."
+        "message": "Only service admins can manage built-in IdP users and 
groups."
+      }
+
+    IdpUnsupportedApiVersionException:
+      value: {
+        "code": 1001,
+        "type": "IllegalArgumentException",
+        "message": "Unsupported version v99 in request header"
       }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
index 35a2831b5a..8e1be73c67 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
@@ -19,6 +19,7 @@
 package org.apache.gravitino.idp.web;
 
 import java.security.PrivilegedExceptionAction;
+import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -81,6 +82,22 @@ public final class IdpRESTUtils {
     return toErrorResponse(errorMsg, e);
   }
 
+  /**
+   * Returns a 400 response when {@code request} is null; otherwise {@code 
null}.
+   *
+   * @param request the deserialized request body, which may be null
+   * @return a bad-request response if {@code request} is null, otherwise 
{@code null}
+   */
+  @Nullable
+  public static Response rejectNullRequestBody(Object request) {
+    if (request != null) {
+      return null;
+    }
+    LOG.warn("Received IdP request with null request body");
+    return illegalArguments(
+        "Request body cannot be null", new IllegalArgumentException("Request 
body cannot be null"));
+  }
+
   public static Response illegalArguments(String message, Throwable throwable) 
{
     return json(
         Response.Status.BAD_REQUEST,
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
index f2ad5233d4..e98ffbd656 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
@@ -77,6 +77,10 @@ public class IdpGroupOperations {
   @Timed(name = "add-idp-group." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "add-idp-group", absolute = true)
   public Response addGroup(AddGroupRequest request) {
+    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
+    if (rejection != null) {
+      return rejection;
+    }
     return IdpRESTUtils.doAs(
         httpRequest,
         () -> {
@@ -111,6 +115,10 @@ public class IdpGroupOperations {
   @ResponseMetered(name = "change-idp-group-users", absolute = true)
   public Response changeGroupMembership(
       @PathParam("group") String group, GroupMembershipChangeRequest request) {
+    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
+    if (rejection != null) {
+      return rejection;
+    }
     return IdpRESTUtils.doAs(
         httpRequest,
         () -> {
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
index 0335b41cf5..78c6e5dd15 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpUserOperations.java
@@ -81,6 +81,10 @@ public class IdpUserOperations {
   @Timed(name = "add-idp-user." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "add-idp-user", absolute = true)
   public Response addUser(AddUserRequest request) {
+    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
+    if (rejection != null) {
+      return rejection;
+    }
     return IdpRESTUtils.doAs(
         httpRequest,
         () -> {
@@ -108,6 +112,10 @@ public class IdpUserOperations {
   @Timed(name = "change-idp-user-password." + 
MetricNames.HTTP_PROCESS_DURATION, absolute = true)
   @ResponseMetered(name = "change-idp-user-password", absolute = true)
   public Response changePassword(@PathParam("user") String user, 
ChangePasswordRequest request) {
+    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
+    if (rejection != null) {
+      return rejection;
+    }
     return IdpRESTUtils.doAs(
         httpRequest,
         () -> {
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
index 42580f0861..b89b897f4a 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpOperations.java
@@ -120,6 +120,14 @@ class TestIdpOperations extends JerseyTest {
     return resourceConfig;
   }
 
+  @Test
+  void testNullRequestBodyReturnsBadRequest() {
+    assertNullBody(postNull("/idp/users"));
+    assertNullBody(putNull("/idp/users/user1/password"));
+    assertNullBody(postNull("/idp/groups"));
+    assertNullBody(putNull("/idp/groups/group1/users"));
+  }
+
   @Test
   void testAddUser() throws Exception {
     AddUserRequest req = new AddUserRequest("user1", VALID_PASSWORD);
@@ -252,10 +260,18 @@ class TestIdpOperations extends JerseyTest {
     return request(path).post(Entity.entity(body, 
MediaType.APPLICATION_JSON_TYPE));
   }
 
+  private Response postNull(String path) {
+    return request(path).post(Entity.entity("null", 
MediaType.APPLICATION_JSON_TYPE));
+  }
+
   private Response put(String path, Object body) {
     return request(path).put(Entity.entity(body, 
MediaType.APPLICATION_JSON_TYPE));
   }
 
+  private Response putNull(String path) {
+    return request(path).put(Entity.entity("null", 
MediaType.APPLICATION_JSON_TYPE));
+  }
+
   private Response delete(String path) {
     return request(path).delete();
   }
@@ -269,6 +285,13 @@ class TestIdpOperations extends JerseyTest {
     Assertions.assertEquals(code, 
response.readEntity(ErrorResponse.class).getCode());
   }
 
+  private void assertNullBody(Response response) {
+    assertStatus(Response.Status.BAD_REQUEST, response);
+    ErrorResponse error = response.readEntity(ErrorResponse.class);
+    Assertions.assertEquals(ErrorConstants.ILLEGAL_ARGUMENTS_CODE, 
error.getCode());
+    Assertions.assertTrue(error.getMessage().contains("Request body cannot be 
null"));
+  }
+
   private IdpUser buildUser(String user) {
     return new IdpUser(user, Collections.emptyList());
   }

Reply via email to