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 9024b6827a [#12978] feat(idp): add audit_info and allow self user 
get/password change (#12979)
9024b6827a is described below

commit 9024b6827adf86273862827760e589e476207111
Author: MaSai <[email protected]>
AuthorDate: Tue Sep 8 20:33:16 2026 +0800

    [#12978] feat(idp): add audit_info and allow self user get/password change 
(#12979)
    
    ### What changes were proposed in this pull request?
    
    - Add `audit_info` to `idp_user_meta`, `idp_group_meta`, and
    `idp_user_group_rel` in schema 2.0.0 and 1.3→2.0 upgrade scripts
    (MySQL/H2/PostgreSQL)
    - Wire audit through PO/mapper/service layers; create paths write
    creator/createTime; password/enabled updates refresh lastModifier
    - Expose `audit` on IdP user/group models, DTOs, and OpenAPI
    - Allow authenticated users to `GET` / `PUT /api/idp/users/{user}` for
    their own username (read profile / change password); `enabled` and other
    IdP management APIs remain service-admin only
    - Fix MySQL upgrade DDL so `MEDIUMTEXT audit_info` is added without an
    illegal default value (H2/PostgreSQL keep `NOT NULL DEFAULT '{}'`)
    - Align markdown tables in the local users/groups docs
    
    ### Why are the changes needed?
    
    Built-in IdP tables previously lacked audit fields used elsewhere in
    Gravitino, so create/update could not record who changed IdP users,
    groups, or memberships. Account owners should also be able to read their
    own IdP profile and change their own password, not only service admins.
    
    Fix: #12978
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes.
    1. IdP user and group API responses include an `audit` field.
    2. Authenticated non-admin users may `GET` and `PUT
    /api/idp/users/{user}` for themselves (password-only on PUT).
    
    ### How was this patch tested?
    
    ```bash
    ./gradlew :plugins:idp-basic:test --tests 
'org.apache.gravitino.idp.web.rest.*' -PskipITs
    ```
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 docs/open-api/idp/idp.yaml                         | 56 +++++++++++++---
 docs/security/local-users-and-groups.md            | 62 ++++++++++-------
 .../apache/gravitino/idp/IdpUserGroupManager.java  |  7 +-
 .../org/apache/gravitino/idp/dto/IdpGroupDTO.java  | 15 ++++-
 .../org/apache/gravitino/idp/dto/IdpUserDTO.java   | 15 ++++-
 .../org/apache/gravitino/idp/model/IdpGroup.java   | 49 ++++++++++++--
 .../org/apache/gravitino/idp/model/IdpUser.java    | 71 +++++++++++++++++---
 .../idp/storage/mapper/IdpUserMetaMapper.java      |  8 ++-
 .../mapper/IdpUserMetaSQLProviderFactory.java      | 12 ++--
 .../provider/base/IdpGroupMetaBaseSQLProvider.java |  9 ++-
 .../base/IdpUserGroupRelBaseSQLProvider.java       |  4 +-
 .../provider/base/IdpUserMetaBaseSQLProvider.java  | 25 ++++---
 .../mapper/provider/h2/IdpGroupMetaH2Provider.java |  3 +-
 .../mapper/provider/h2/IdpUserMetaH2Provider.java  |  4 +-
 .../postgresql/IdpGroupMetaPostgreSQLProvider.java |  3 +-
 .../postgresql/IdpUserMetaPostgreSQLProvider.java  |  4 +-
 .../gravitino/idp/storage/po/IdpGroupPO.java       | 22 ++++++-
 .../idp/storage/po/IdpGroupWithUsersPO.java        |  1 +
 .../idp/storage/po/IdpUserGroupRelPO.java          | 22 ++++++-
 .../apache/gravitino/idp/storage/po/IdpUserPO.java | 24 ++++++-
 .../idp/storage/po/IdpUserWithGroupsPO.java        |  1 +
 .../storage/relational/utils/IdpPOConverters.java  | 77 +++++++++++++++++++++-
 .../idp/storage/service/IdpGroupMetaService.java   |  1 +
 .../idp/storage/service/IdpUserMetaService.java    | 10 ++-
 .../org/apache/gravitino/idp/web/IdpRESTUtils.java |  4 ++
 .../idp/web/rest/IdpAuthorizationFilter.java       | 40 +++++++++--
 .../gravitino/idp/web/rest/IdpUserOperations.java  | 27 ++++++++
 .../gravitino/idp/TestIdpUserGroupManager.java     | 21 +++++-
 .../apache/gravitino/idp/dto/TestIdpGroupDTO.java  | 12 +++-
 .../apache/gravitino/idp/dto/TestIdpUserDTO.java   | 12 +++-
 .../idp/integration/test/IdpRESTApiIT.java         | 46 ++++++++++++-
 .../storage/mapper/TestIdpGroupMetaStorage.java    | 13 ++++
 .../storage/mapper/TestIdpUserGroupRelStorage.java | 32 +++++++++
 .../idp/storage/mapper/TestIdpUserMetaStorage.java | 22 +++++--
 .../gravitino/idp/storage/po/TestIdpGroupPO.java   | 20 ++++++
 .../idp/storage/po/TestIdpUserGroupRelPO.java      | 20 ++++++
 .../gravitino/idp/storage/po/TestIdpUserPO.java    | 21 ++++++
 .../relational/TestIdpGarbageCollector.java        |  3 +
 .../service/AbstractIdpMetaServiceTest.java        |  3 +
 .../storage/service/TestIdpGroupMetaService.java   |  5 ++
 .../storage/service/TestIdpUserMetaService.java    |  2 +
 .../idp/web/rest/TestIdpAuthorizationFilter.java   | 42 ++++++++++++
 .../gravitino/idp/web/rest/TestIdpOperations.java  | 25 +++++++
 scripts/h2/schema-2.0.0-h2.sql                     |  3 +
 scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql           |  6 ++
 scripts/mysql/schema-2.0.0-mysql.sql               |  3 +
 scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql     | 31 +++++++++
 scripts/postgresql/schema-2.0.0-postgresql.sql     |  6 ++
 .../upgrade-1.3.0-to-2.0.0-postgresql.sql          |  9 +++
 49 files changed, 825 insertions(+), 108 deletions(-)

diff --git a/docs/open-api/idp/idp.yaml b/docs/open-api/idp/idp.yaml
index 71e8ff581a..a1b4c0d8fa 100644
--- a/docs/open-api/idp/idp.yaml
+++ b/docs/open-api/idp/idp.yaml
@@ -80,7 +80,10 @@ paths:
       tags:
         - IDP
       summary: Get local user store user
-      description: Returns the specified local user store user, including 
group membership (empty if none; the example shows a user who already belongs 
to groups).
+      description: >
+        Returns the specified local user store user, including group 
membership (empty if none; the
+        example shows a user who already belongs to groups). Service admins 
may get any user.
+        Authenticated non-admin users may get only their own user.
       operationId: getIdpUser
       responses:
         "200":
@@ -114,6 +117,8 @@ paths:
         Updates the password and/or enabled flag of the specified local user 
store user and returns
         the user object. At least one of `password` or `enabled` is required. 
Disabled users cannot
         authenticate. Users listed in `gravitino.authorization.serviceAdmins` 
cannot be disabled.
+        Service admins may update any user. Authenticated non-admin users may 
update only their own
+        password and cannot change `enabled` or another user's account.
       operationId: updateIdpUser
       requestBody:
         required: true
@@ -356,6 +361,8 @@ components:
             type: string
             maxLength: 128
           description: The local user store groups the user belongs to
+        audit:
+          $ref: "../openapi.yaml#/components/schemas/Audit"
 
     IdpGroup:
       type: object
@@ -376,6 +383,8 @@ components:
             type: string
             maxLength: 128
           description: The usernames of members in the local user store group
+        audit:
+          $ref: "../openapi.yaml#/components/schemas/Audit"
 
     AddUserRequest:
       type: object
@@ -487,9 +496,10 @@ components:
   responses:
     IdpForbiddenErrorResponse:
       description:
-        Forbidden - Only callers listed in 
`gravitino.authorization.serviceAdmins` may
-        manage local user store users and groups. Unauthenticated requests and 
authenticated
-        non-service-admin users receive this response.
+        Forbidden - Callers listed in `gravitino.authorization.serviceAdmins` 
may manage local
+        user store users and groups. Authenticated non-admin users may only 
`GET` or `PUT`
+        `/idp/users/{user}` for their own username (password-only on update). 
Unauthenticated
+        requests and other non-admin IdP management calls receive this 
response.
       content:
         application/vnd.gravitino.v1+json:
           schema:
@@ -533,7 +543,11 @@ components:
         "user": {
           "name": "alice",
           "enabled": true,
-          "groups": []
+          "groups": [],
+          "audit": {
+            "creator": "gravitino",
+            "createTime": "2023-12-08T06:41:25.595Z"
+          }
         }
       }
 
@@ -543,7 +557,11 @@ components:
         "user": {
           "name": "alice",
           "enabled": true,
-          "groups": ["engineers"]
+          "groups": ["engineers"],
+          "audit": {
+            "creator": "gravitino",
+            "createTime": "2023-12-08T06:41:25.595Z"
+          }
         }
       }
 
@@ -553,7 +571,13 @@ components:
         "user": {
           "name": "alice",
           "enabled": true,
-          "groups": ["engineers"]
+          "groups": ["engineers"],
+          "audit": {
+            "creator": "gravitino",
+            "createTime": "2023-12-08T06:41:25.595Z",
+            "lastModifier": "gravitino",
+            "lastModifiedTime": "2023-12-08T07:10:11.123Z"
+          }
         }
       }
 
@@ -563,7 +587,11 @@ components:
         "group": {
           "name": "engineers",
           "comment": "Platform engineering",
-          "users": []
+          "users": [],
+          "audit": {
+            "creator": "gravitino",
+            "createTime": "2023-12-08T06:41:25.595Z"
+          }
         }
       }
 
@@ -573,7 +601,11 @@ components:
         "group": {
           "name": "engineers",
           "comment": "Platform engineering",
-          "users": ["alice", "bob"]
+          "users": ["alice", "bob"],
+          "audit": {
+            "creator": "gravitino",
+            "createTime": "2023-12-08T06:41:25.595Z"
+          }
         }
       }
 
@@ -583,7 +615,11 @@ components:
         "group": {
           "name": "engineers",
           "comment": "Platform engineering",
-          "users": ["alice", "bob"]
+          "users": ["alice", "bob"],
+          "audit": {
+            "creator": "gravitino",
+            "createTime": "2023-12-08T06:41:25.595Z"
+          }
         }
       }
 
diff --git a/docs/security/local-users-and-groups.md 
b/docs/security/local-users-and-groups.md
index c497b59a8b..70b18c9e23 100644
--- a/docs/security/local-users-and-groups.md
+++ b/docs/security/local-users-and-groups.md
@@ -54,11 +54,11 @@ curl -s -X POST -H "Accept: 
application/vnd.gravitino.v1+json" \
 
 ## Configuration
 
-| Configuration Item                        | Description                      
                                 | Example                                     |
-|-------------------------------------------|-------------------------------------------------------------------|---------------------------------------------|
-| `gravitino.authenticators`                | Must be `basic`, and must not 
include `simple`                    | `basic`                                   
  |
-| `gravitino.server.rest.extensionPackages` | Registers the user and group 
management endpoints                 | 
`org.apache.gravitino.idp.web.rest.feature` |
-| `gravitino.authorization.serviceAdmins`   | Comma-separated usernames 
allowed to manage users and groups      | `admin`                               
      |
+| Configuration Item                        | Description                      
                                    | Example                                   
  |
+|-------------------------------------------|----------------------------------------------------------------------|---------------------------------------------|
+| `gravitino.authenticators`                | Must be `basic`, and must not 
include `simple`                       | `basic`                                
     |
+| `gravitino.server.rest.extensionPackages` | Registers the user and group 
management endpoints                    | 
`org.apache.gravitino.idp.web.rest.feature` |
+| `gravitino.authorization.serviceAdmins`   | Comma-separated usernames 
allowed to manage all IdP users and groups | `admin`                            
         |
 
 The local user store is incompatible with the `simple` authenticator, which is 
the server default and accepts the username a client supplies without checking 
a password. Both authenticators claim the same `Basic` authorization header, 
and the server uses the first one listed that claims it, so listing `simple` 
ahead of `basic` means passwords are never checked. Replace `simple` rather 
than adding to it.
 
@@ -68,27 +68,41 @@ The Web UI reads `gravitino.authenticators` from the server 
and presents a usern
 
 These rules apply to user creation, password changes, and 
`GRAVITINO_INITIAL_ADMIN_PASSWORD` alike.
 
-| Rule            | Value                                     |
-|-----------------|-------------------------------------------|
-| Username        | Required, and must not contain a colon    |
-| Password length | 12 to 64 characters inclusive             |
+| Rule            | Value                                  |
+|-----------------|----------------------------------------|
+| Username        | Required, and must not contain a colon |
+| Password length | 12 to 64 characters inclusive          |
 
-Passwords are reset by an administrator rather than changed by the user, so a 
password change request carries the new password only and no current password.
+Passwords may be reset by a service admin or changed by the user for their own 
account.
+The request carries the new password only and no current password; callers 
authenticate with
+HTTP Basic using their current credentials.
 
 ## Managing Users and Groups
 
-All management endpoints are under `http://{host}:{port}/api/idp` and require 
Basic authentication as a service admin. Send `Accept: 
application/vnd.gravitino.v1+json` on every request, and `Content-Type: 
application/json` on requests with a body.
+All management endpoints are under `http://{host}:{port}/api/idp` and require 
Basic authentication.
+Most operations require a service admin. Authenticated users may also `GET` or 
`PUT`
+`/api/idp/users/{user}` for their own username (read profile / change password 
only).
+Send `Accept: application/vnd.gravitino.v1+json` on every request, and
+`Content-Type: application/json` on requests with a body.
 
 ### User Operations
 
-| Operation                | Method | Path                                | 
Body                                       |
-|--------------------------|--------|-------------------------------------|--------------------------------------------|
-| Get a user               | GET    | `/api/idp/users/{user}`             | 
None                                       |
-| Add a user               | POST   | `/api/idp/users`                    | 
`{"user":"alice","password":"{password}"}` |
-| Update a user            | PUT    | `/api/idp/users/{user}`             | 
`{"password":"{new_password}"}` and/or `{"enabled":false}` |
-| Remove a user            | DELETE | `/api/idp/users/{user}`             | 
None                                       |
+| Operation     | Method | Path                    | Body                      
                                 |
+|---------------|--------|-------------------------|------------------------------------------------------------|
+| Get a user    | GET    | `/api/idp/users/{user}` | None                      
                                 |
+| Add a user    | POST   | `/api/idp/users`        | 
`{"user":"alice","password":"{password}"}`                 |
+| Update a user | PUT    | `/api/idp/users/{user}` | 
`{"password":"{new_password}"}` and/or `{"enabled":false}` |
+| Remove a user | DELETE | `/api/idp/users/{user}` | None                      
                                 |
 
-The add-user body uses the field name `user` rather than `name`. `enabled` is 
optional on create and defaults to `true`. A disabled user cannot authenticate. 
`PUT /api/idp/users/{user}` accepts `password` and/or `enabled`; at least one 
is required. Users listed in `gravitino.authorization.serviceAdmins` cannot be 
disabled.
+The add-user body uses the field name `user` rather than `name`. `enabled` is 
optional on create and defaults to `true`. A disabled user cannot authenticate. 
`PUT /api/idp/users/{user}` accepts `password` and/or `enabled`; at least one 
is required. Users listed in `gravitino.authorization.serviceAdmins` cannot be 
disabled. Non-admin callers may get or update only their own user, and on 
update may change only their password (not `enabled` or another user).
+
+```shell
+curl -s -X PUT -H "Accept: application/vnd.gravitino.v1+json" \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Basic $(echo -n 'alice:{user_password}' | base64)" \
+  -d '{"password":"{new_password}"}' \
+  http://localhost:8090/api/idp/users/alice
+```
 
 ```shell
 curl -s -X POST -H "Accept: application/vnd.gravitino.v1+json" \
@@ -100,12 +114,12 @@ curl -s -X POST -H "Accept: 
application/vnd.gravitino.v1+json" \
 
 ### Group Operations
 
-| Operation                | Method | Path                                     
   | Body                                                       |
-|--------------------------|--------|---------------------------------------------|------------------------------------------------------------|
-| Get a group              | GET    | `/api/idp/groups/{group}`                
   | None                                                       |
-| Add a group              | POST   | `/api/idp/groups`                        
   | `{"group":"engineering","comment":"Platform engineering"}` |
-| Remove a group           | DELETE | `/api/idp/groups/{group}?force={true 
false}`| None                                                       |
-| Change group membership  | PUT    | `/api/idp/groups/{group}/users`          
   | `{"usersToAdd":["alice"],"usersToRemove":["carol"]}`       |
+| Operation               | Method | Path                                      
   | Body                                                       |
+|-------------------------|--------|----------------------------------------------|------------------------------------------------------------|
+| Get a group             | GET    | `/api/idp/groups/{group}`                 
   | None                                                       |
+| Add a group             | POST   | `/api/idp/groups`                         
   | `{"group":"engineering","comment":"Platform engineering"}` |
+| Remove a group          | DELETE | `/api/idp/groups/{group}?force={true 
false}` | None                                                       |
+| Change group membership | PUT    | `/api/idp/groups/{group}/users`           
   | `{"usersToAdd":["alice"],"usersToRemove":["carol"]}`       |
 
 The add-group body uses the field name `group` rather than `name`. `comment` 
is optional on create (max 1024 characters, utf8mb4) and stored as the group 
description. Removing a group that still has members fails unless `force=true`. 
A membership change requires at least one of `usersToAdd` or `usersToRemove`, 
and accepts both in a single request.
 
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
index 7fcc858699..63300314fb 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java
@@ -40,6 +40,7 @@ import org.apache.gravitino.idp.storage.po.IdpGroupPO;
 import org.apache.gravitino.idp.storage.po.IdpUserPO;
 import org.apache.gravitino.idp.storage.relational.IdpGarbageCollector;
 import org.apache.gravitino.idp.storage.relational.IdpRelationalStorage;
+import org.apache.gravitino.idp.storage.relational.utils.IdpPOConverters;
 import org.apache.gravitino.idp.storage.service.IdpGroupMetaService;
 import org.apache.gravitino.idp.storage.service.IdpUserMetaService;
 import org.apache.gravitino.storage.IdGenerator;
@@ -144,7 +145,7 @@ public class IdpUserGroupManager implements Closeable {
     checkCanDisableUser(username, enabled);
     String passwordHash = passwordHasher.hash(password);
     USER_SERVICE.insertIdpUser(newUserPO(username, passwordHash, enabled));
-    return new IdpUser(username, Collections.emptyList(), enabled);
+    return USER_SERVICE.getIdpUser(username);
   }
 
   /**
@@ -231,7 +232,7 @@ public class IdpUserGroupManager implements Closeable {
     IdpCredentialValidator.validateGroupComment(comment);
     String normalizedComment = comment == null ? "" : comment;
     GROUP_SERVICE.insertIdpGroup(newGroupPO(groupName, normalizedComment));
-    return new IdpGroup(groupName, Collections.emptyList(), normalizedComment);
+    return GROUP_SERVICE.getIdpGroup(groupName);
   }
 
   /**
@@ -304,6 +305,7 @@ public class IdpUserGroupManager implements Closeable {
         .withUsername(username)
         .withPasswordHash(passwordHash)
         .withEnabled(enabled)
+        .withAuditInfo(IdpPOConverters.newCreateAuditInfoJson())
         .withCurrentVersion(POConverters.INIT_VERSION)
         .withLastVersion(POConverters.INIT_VERSION)
         .withDeletedAt(POConverters.DEFAULT_DELETED_AT)
@@ -324,6 +326,7 @@ public class IdpUserGroupManager implements Closeable {
         .withGroupId(idGenerator.nextId())
         .withGroupName(groupName)
         .withGroupComment(comment == null ? "" : comment)
+        .withAuditInfo(IdpPOConverters.newCreateAuditInfoJson())
         .withCurrentVersion(POConverters.INIT_VERSION)
         .withLastVersion(POConverters.INIT_VERSION)
         .withDeletedAt(POConverters.DEFAULT_DELETED_AT)
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpGroupDTO.java 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpGroupDTO.java
index 121e7b6c61..2d8744dde5 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpGroupDTO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpGroupDTO.java
@@ -31,6 +31,7 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.ToString;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.AuditDTO;
 
 /** Represents a built-in IdP group Data Transfer Object (DTO). */
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -49,15 +50,19 @@ public class IdpGroupDTO {
   @JsonSetter(nulls = Nulls.AS_EMPTY)
   private List<String> users = Collections.emptyList();
 
+  @JsonProperty("audit")
+  private AuditDTO audit;
+
   /**
    * Creates a new instance of IdpGroupDTO.
    *
    * @param name The name of the built-in IdP group DTO.
    * @param comment The comment of the built-in IdP group DTO.
    * @param users The users of the built-in IdP group DTO.
+   * @param audit The audit information of the built-in IdP group DTO.
    */
   @Builder(setterPrefix = "with")
-  protected IdpGroupDTO(String name, String comment, List<String> users) {
+  protected IdpGroupDTO(String name, String comment, List<String> users, 
AuditDTO audit) {
     Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot be 
null or empty");
     if (users != null) {
       users.forEach(
@@ -68,6 +73,7 @@ public class IdpGroupDTO {
     this.name = name;
     this.comment = comment == null ? "" : comment;
     this.users = users == null ? Collections.emptyList() : users;
+    this.audit = audit;
   }
 
   /**
@@ -92,4 +98,11 @@ public class IdpGroupDTO {
   public List<String> users() {
     return users;
   }
+
+  /**
+   * @return The audit information of the built-in IdP group DTO.
+   */
+  public AuditDTO audit() {
+    return audit;
+  }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java
index b23df5f20d..e6790da808 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/dto/IdpUserDTO.java
@@ -31,6 +31,7 @@ import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import lombok.ToString;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.AuditDTO;
 
 /** Represents a built-in IdP user Data Transfer Object (DTO). */
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
@@ -48,15 +49,19 @@ public class IdpUserDTO {
   @JsonSetter(nulls = Nulls.AS_EMPTY)
   private List<String> groups = Collections.emptyList();
 
+  @JsonProperty("audit")
+  private AuditDTO audit;
+
   /**
    * Creates a new instance of IdpUserDTO.
    *
    * @param name The name of the built-in IdP user DTO.
    * @param enabled Whether the built-in IdP user is enabled.
    * @param groups The groups of the built-in IdP user DTO.
+   * @param audit The audit information of the built-in IdP user DTO.
    */
   @Builder(setterPrefix = "with")
-  protected IdpUserDTO(String name, Boolean enabled, List<String> groups) {
+  protected IdpUserDTO(String name, Boolean enabled, List<String> groups, 
AuditDTO audit) {
     Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot be 
null or empty");
     if (groups != null) {
       groups.forEach(
@@ -68,6 +73,7 @@ public class IdpUserDTO {
     this.name = name;
     this.enabled = enabled == null || enabled;
     this.groups = groups == null ? Collections.emptyList() : groups;
+    this.audit = audit;
   }
 
   /**
@@ -92,4 +98,11 @@ public class IdpUserDTO {
   public List<String> groups() {
     return groups;
   }
+
+  /**
+   * @return The audit information of the built-in IdP user DTO.
+   */
+  public AuditDTO audit() {
+    return audit;
+  }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpGroup.java 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpGroup.java
index 9ac51eb3ac..abde0d15e6 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpGroup.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpGroup.java
@@ -20,14 +20,19 @@ package org.apache.gravitino.idp.model;
 
 import java.util.List;
 import java.util.Objects;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.idp.dto.IdpGroupDTO;
+import org.apache.gravitino.meta.AuditInfo;
 
 /** Built-in IdP group. */
-public class IdpGroup {
+public class IdpGroup implements Auditable {
 
   private final String name;
   private final String comment;
   private final List<String> usernames;
+  private final AuditInfo auditInfo;
 
   /**
    * Creates a built-in IdP group.
@@ -36,7 +41,7 @@ public class IdpGroup {
    * @param usernames The usernames in the group.
    */
   public IdpGroup(String name, List<String> usernames) {
-    this(name, usernames, "");
+    this(name, usernames, "", AuditInfo.EMPTY);
   }
 
   /**
@@ -47,9 +52,22 @@ public class IdpGroup {
    * @param comment The group comment, or empty if none.
    */
   public IdpGroup(String name, List<String> usernames, String comment) {
+    this(name, usernames, comment, AuditInfo.EMPTY);
+  }
+
+  /**
+   * Creates a built-in IdP group.
+   *
+   * @param name The group name.
+   * @param usernames The usernames in the group.
+   * @param comment The group comment, or empty if none.
+   * @param auditInfo Audit information.
+   */
+  public IdpGroup(String name, List<String> usernames, String comment, 
AuditInfo auditInfo) {
     this.name = name;
     this.usernames = usernames;
     this.comment = comment == null ? "" : comment;
+    this.auditInfo = auditInfo == null ? AuditInfo.EMPTY : auditInfo;
   }
 
   /** Returns the group name. */
@@ -67,13 +85,23 @@ public class IdpGroup {
     return usernames;
   }
 
+  @Override
+  public Audit auditInfo() {
+    return auditInfo;
+  }
+
   /**
    * Converts this group to a REST DTO.
    *
    * @return the group DTO
    */
   public IdpGroupDTO toDTO() {
-    return 
IdpGroupDTO.builder().withName(name).withComment(comment).withUsers(usernames).build();
+    return IdpGroupDTO.builder()
+        .withName(name)
+        .withComment(comment)
+        .withUsers(usernames)
+        .withAudit(DTOConverters.toDTO(auditInfo))
+        .build();
   }
 
   @Override
@@ -87,16 +115,25 @@ public class IdpGroup {
     IdpGroup that = (IdpGroup) other;
     return Objects.equals(name, that.name)
         && Objects.equals(comment, that.comment)
-        && Objects.equals(usernames, that.usernames);
+        && Objects.equals(usernames, that.usernames)
+        && Objects.equals(auditInfo, that.auditInfo);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(name, comment, usernames);
+    return Objects.hash(name, comment, usernames, auditInfo);
   }
 
   @Override
   public String toString() {
-    return "IdpGroup{name='" + name + "', comment='" + comment + "', 
usernames=" + usernames + '}';
+    return "IdpGroup{name='"
+        + name
+        + "', comment='"
+        + comment
+        + "', usernames="
+        + usernames
+        + ", auditInfo="
+        + auditInfo
+        + '}';
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
index ae698f9b40..6c4db20573 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/model/IdpUser.java
@@ -22,15 +22,20 @@ import com.google.common.base.Preconditions;
 import java.util.List;
 import java.util.Objects;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.Audit;
+import org.apache.gravitino.Auditable;
+import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.idp.dto.IdpUserDTO;
+import org.apache.gravitino.meta.AuditInfo;
 
 /** Built-in IdP user. */
-public class IdpUser {
+public class IdpUser implements Auditable {
 
   private final String name;
   private final String passwordHash;
   private final boolean enabled;
   private final List<String> groupNames;
+  private final AuditInfo auditInfo;
 
   /**
    * Creates a built-in IdP user without a password hash.
@@ -39,7 +44,7 @@ public class IdpUser {
    * @param groupNames The group names the user belongs to.
    */
   public IdpUser(String name, List<String> groupNames) {
-    this(name, null, groupNames, true);
+    this(name, null, groupNames, true, AuditInfo.EMPTY);
   }
 
   /**
@@ -50,7 +55,19 @@ public class IdpUser {
    * @param enabled Whether the user is enabled.
    */
   public IdpUser(String name, List<String> groupNames, boolean enabled) {
-    this(name, null, groupNames, enabled);
+    this(name, null, groupNames, enabled, AuditInfo.EMPTY);
+  }
+
+  /**
+   * Creates a built-in IdP user without a password hash.
+   *
+   * @param name The username.
+   * @param groupNames The group names the user belongs to.
+   * @param enabled Whether the user is enabled.
+   * @param auditInfo Audit information.
+   */
+  public IdpUser(String name, List<String> groupNames, boolean enabled, 
AuditInfo auditInfo) {
+    this(name, null, groupNames, enabled, auditInfo);
   }
 
   /**
@@ -61,7 +78,7 @@ public class IdpUser {
    * @param groupNames The group names the user belongs to.
    */
   public IdpUser(String name, String passwordHash, List<String> groupNames) {
-    this(name, passwordHash, groupNames, true);
+    this(name, passwordHash, groupNames, true, AuditInfo.EMPTY);
   }
 
   /**
@@ -73,6 +90,24 @@ public class IdpUser {
    * @param enabled Whether the user is enabled.
    */
   public IdpUser(String name, String passwordHash, List<String> groupNames, 
boolean enabled) {
+    this(name, passwordHash, groupNames, enabled, AuditInfo.EMPTY);
+  }
+
+  /**
+   * Creates a built-in IdP user with a password hash loaded from storage.
+   *
+   * @param name The username.
+   * @param passwordHash The password hash, or null when the hash is not 
loaded.
+   * @param groupNames The group names the user belongs to.
+   * @param enabled Whether the user is enabled.
+   * @param auditInfo Audit information.
+   */
+  public IdpUser(
+      String name,
+      String passwordHash,
+      List<String> groupNames,
+      boolean enabled,
+      AuditInfo auditInfo) {
     if (passwordHash != null) {
       Preconditions.checkArgument(
           StringUtils.isNotBlank(passwordHash), "passwordHash must not be 
blank");
@@ -81,6 +116,7 @@ public class IdpUser {
     this.passwordHash = passwordHash;
     this.enabled = enabled;
     this.groupNames = groupNames;
+    this.auditInfo = auditInfo == null ? AuditInfo.EMPTY : auditInfo;
   }
 
   /** Returns the username. */
@@ -111,13 +147,23 @@ public class IdpUser {
     return groupNames;
   }
 
+  @Override
+  public Audit auditInfo() {
+    return auditInfo;
+  }
+
   /**
    * Converts this user to a REST DTO.
    *
    * @return the user DTO
    */
   public IdpUserDTO toDTO() {
-    return 
IdpUserDTO.builder().withName(name).withEnabled(enabled).withGroups(groupNames).build();
+    return IdpUserDTO.builder()
+        .withName(name)
+        .withEnabled(enabled)
+        .withGroups(groupNames)
+        .withAudit(DTOConverters.toDTO(auditInfo))
+        .build();
   }
 
   @Override
@@ -132,16 +178,25 @@ public class IdpUser {
     return enabled == that.enabled
         && Objects.equals(name, that.name)
         && Objects.equals(passwordHash, that.passwordHash)
-        && Objects.equals(groupNames, that.groupNames);
+        && Objects.equals(groupNames, that.groupNames)
+        && Objects.equals(auditInfo, that.auditInfo);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(name, passwordHash, enabled, groupNames);
+    return Objects.hash(name, passwordHash, enabled, groupNames, auditInfo);
   }
 
   @Override
   public String toString() {
-    return "IdpUser{name='" + name + "', enabled=" + enabled + ", groupNames=" 
+ groupNames + '}';
+    return "IdpUser{name='"
+        + name
+        + "', enabled="
+        + enabled
+        + ", groupNames="
+        + groupNames
+        + ", auditInfo="
+        + auditInfo
+        + '}';
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
index 55482f5697..28aacb2feb 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaMapper.java
@@ -53,11 +53,15 @@ public interface IdpUserMetaMapper {
 
   @UpdateProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"updateIdpUserPassword")
   Integer updateIdpUserPassword(
-      @Param("username") String username, @Param("passwordHash") String 
passwordHash);
+      @Param("username") String username,
+      @Param("passwordHash") String passwordHash,
+      @Param("auditInfo") String auditInfo);
 
   @UpdateProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"updateIdpUserEnabled")
   Integer updateIdpUserEnabled(
-      @Param("username") String username, @Param("enabled") boolean enabled);
+      @Param("username") String username,
+      @Param("enabled") boolean enabled,
+      @Param("auditInfo") String auditInfo);
 
   @UpdateProvider(type = IdpUserMetaSQLProviderFactory.class, method = 
"softDeleteIdpUser")
   Integer softDeleteIdpUser(@Param("username") String username);
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
index e165749311..79697a04aa 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/IdpUserMetaSQLProviderFactory.java
@@ -62,13 +62,17 @@ public class IdpUserMetaSQLProviderFactory {
   }
 
   public static String updateIdpUserPassword(
-      @Param("username") String username, @Param("passwordHash") String 
passwordHash) {
-    return currentProvider().updateIdpUserPassword(username, passwordHash);
+      @Param("username") String username,
+      @Param("passwordHash") String passwordHash,
+      @Param("auditInfo") String auditInfo) {
+    return currentProvider().updateIdpUserPassword(username, passwordHash, 
auditInfo);
   }
 
   public static String updateIdpUserEnabled(
-      @Param("username") String username, @Param("enabled") boolean enabled) {
-    return currentProvider().updateIdpUserEnabled(username, enabled);
+      @Param("username") String username,
+      @Param("enabled") boolean enabled,
+      @Param("auditInfo") String auditInfo) {
+    return currentProvider().updateIdpUserEnabled(username, enabled, 
auditInfo);
   }
 
   public static String softDeleteIdpUser(@Param("username") String username) {
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpGroupMetaBaseSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpGroupMetaBaseSQLProvider.java
index 6533b57f60..0d8104bcf6 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpGroupMetaBaseSQLProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpGroupMetaBaseSQLProvider.java
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.gravitino.idp.storage.mapper.provider.base;
 
 import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper;
@@ -31,6 +30,7 @@ public class IdpGroupMetaBaseSQLProvider {
   public String selectIdpGroup(@Param("groupName") String groupName) {
     return "SELECT group_id as groupId, group_name as groupName,"
         + " COALESCE(group_comment, '') as groupComment,"
+        + " audit_info as auditInfo,"
         + " current_version as currentVersion,"
         + " last_version as lastVersion, deleted_at as deletedAt"
         + " FROM "
@@ -41,6 +41,7 @@ public class IdpGroupMetaBaseSQLProvider {
   public String selectIdpGroupWithUsers(@Param("groupName") String groupName) {
     return "SELECT g.group_name as name,"
         + " COALESCE(g.group_comment, '') as comment,"
+        + " g.audit_info as auditInfo,"
         + " COALESCE(JSON_ARRAYAGG(u.user_name), JSON_ARRAY()) as usernames"
         + " FROM "
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
@@ -51,17 +52,19 @@ public class IdpGroupMetaBaseSQLProvider {
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
         + " u ON u.user_id = r.user_id AND u.deleted_at = 0"
         + " WHERE g.group_name = #{groupName} AND g.deleted_at = 0"
-        + " GROUP BY g.group_id, g.group_name, g.group_comment";
+        + " GROUP BY g.group_id, g.group_name, g.group_comment, g.audit_info";
   }
 
   public String insertIdpGroup(@Param("groupMeta") IdpGroupPO groupPO) {
     return "INSERT INTO "
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
-        + " (group_id, group_name, group_comment, current_version, 
last_version, deleted_at)"
+        + " (group_id, group_name, group_comment, audit_info, current_version, 
last_version,"
+        + " deleted_at)"
         + " VALUES ("
         + " #{groupMeta.groupId},"
         + " #{groupMeta.groupName},"
         + " COALESCE(#{groupMeta.groupComment}, ''),"
+        + " #{groupMeta.auditInfo},"
         + " #{groupMeta.currentVersion},"
         + " #{groupMeta.lastVersion},"
         + " #{groupMeta.deletedAt}"
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserGroupRelBaseSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserGroupRelBaseSQLProvider.java
index 7eb2352569..e440dddcf6 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserGroupRelBaseSQLProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserGroupRelBaseSQLProvider.java
@@ -63,10 +63,10 @@ public class IdpUserGroupRelBaseSQLProvider {
     return "<script>"
         + "INSERT INTO "
         + IdpUserGroupRelMapper.IDP_USER_GROUP_REL_TABLE_NAME
-        + " (id, user_id, group_id, current_version, last_version, deleted_at)"
+        + " (id, user_id, group_id, audit_info, current_version, last_version, 
deleted_at)"
         + " VALUES "
         + "<foreach item='item' collection='relations' separator=','>"
-        + "(#{item.id}, #{item.userId}, #{item.groupId}, 
#{item.currentVersion},"
+        + "(#{item.id}, #{item.userId}, #{item.groupId}, #{item.auditInfo}, 
#{item.currentVersion},"
         + " #{item.lastVersion}, #{item.deletedAt})"
         + "</foreach>"
         + "</script>";
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
index 306ce43439..4ffdb4a513 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/base/IdpUserMetaBaseSQLProvider.java
@@ -16,7 +16,6 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-
 package org.apache.gravitino.idp.storage.mapper.provider.base;
 
 import java.util.List;
@@ -30,7 +29,7 @@ import org.apache.ibatis.annotations.Param;
 public class IdpUserMetaBaseSQLProvider {
   public String selectIdpUser(@Param("username") String username) {
     return "SELECT user_id as userId, user_name as username, password_hash as 
passwordHash,"
-        + " enabled as enabled,"
+        + " enabled as enabled, audit_info as auditInfo,"
         + " current_version as currentVersion,"
         + " last_version as lastVersion, deleted_at as deletedAt"
         + " FROM "
@@ -40,7 +39,7 @@ public class IdpUserMetaBaseSQLProvider {
 
   public String selectIdpUserWithGroups(@Param("username") String username) {
     return "SELECT u.user_name as name, u.password_hash as passwordHash,"
-        + " u.enabled as enabled,"
+        + " u.enabled as enabled, u.audit_info as auditInfo,"
         + " COALESCE(JSON_ARRAYAGG(g.group_name), JSON_ARRAY()) as groupNames"
         + " FROM "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
@@ -51,13 +50,13 @@ public class IdpUserMetaBaseSQLProvider {
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
         + " g ON g.group_id = r.group_id AND g.deleted_at = 0"
         + " WHERE u.user_name = #{username} AND u.deleted_at = 0"
-        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled";
+        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled, 
u.audit_info";
   }
 
   public String selectIdpUsersByUsernames(@Param("usernames") List<String> 
usernames) {
     return "<script>"
         + "SELECT user_id as userId, user_name as username, password_hash as 
passwordHash,"
-        + " enabled as enabled,"
+        + " enabled as enabled, audit_info as auditInfo,"
         + " current_version as currentVersion,"
         + " last_version as lastVersion, deleted_at as deletedAt"
         + " FROM "
@@ -73,12 +72,14 @@ public class IdpUserMetaBaseSQLProvider {
   public String insertIdpUser(@Param("userMeta") IdpUserPO userPO) {
     return "INSERT INTO "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
-        + " (user_id, user_name, password_hash, enabled, current_version, 
last_version, deleted_at)"
+        + " (user_id, user_name, password_hash, enabled, audit_info, 
current_version,"
+        + " last_version, deleted_at)"
         + " VALUES ("
         + " #{userMeta.userId},"
         + " #{userMeta.username},"
         + " #{userMeta.passwordHash},"
         + " #{userMeta.enabled},"
+        + " #{userMeta.auditInfo},"
         + " #{userMeta.currentVersion},"
         + " #{userMeta.lastVersion},"
         + " #{userMeta.deletedAt}"
@@ -86,19 +87,23 @@ public class IdpUserMetaBaseSQLProvider {
   }
 
   public String updateIdpUserPassword(
-      @Param("username") String username, @Param("passwordHash") String 
passwordHash) {
+      @Param("username") String username,
+      @Param("passwordHash") String passwordHash,
+      @Param("auditInfo") String auditInfo) {
     return "UPDATE "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
-        + " SET password_hash = #{passwordHash}"
+        + " SET password_hash = #{passwordHash}, audit_info = #{auditInfo}"
         + " WHERE user_name = #{username}"
         + " AND deleted_at = 0";
   }
 
   public String updateIdpUserEnabled(
-      @Param("username") String username, @Param("enabled") boolean enabled) {
+      @Param("username") String username,
+      @Param("enabled") boolean enabled,
+      @Param("auditInfo") String auditInfo) {
     return "UPDATE "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
-        + " SET enabled = #{enabled}"
+        + " SET enabled = #{enabled}, audit_info = #{auditInfo}"
         + " WHERE user_name = #{username}"
         + " AND deleted_at = 0";
   }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpGroupMetaH2Provider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpGroupMetaH2Provider.java
index cb84d4d98d..7ab0396229 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpGroupMetaH2Provider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpGroupMetaH2Provider.java
@@ -32,6 +32,7 @@ public class IdpGroupMetaH2Provider extends 
IdpGroupMetaBaseSQLProvider {
   public String selectIdpGroupWithUsers(@Param("groupName") String groupName) {
     return "SELECT g.group_name as name,"
         + " COALESCE(g.group_comment, '') as comment,"
+        + " g.audit_info as auditInfo,"
         + " '['"
         + " || COALESCE(GROUP_CONCAT('\"' || u.user_name || '\"'), '')"
         + " || ']' as usernames"
@@ -44,7 +45,7 @@ public class IdpGroupMetaH2Provider extends 
IdpGroupMetaBaseSQLProvider {
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
         + " u ON u.user_id = r.user_id AND u.deleted_at = 0"
         + " WHERE g.group_name = #{groupName} AND g.deleted_at = 0"
-        + " GROUP BY g.group_id, g.group_name, g.group_comment";
+        + " GROUP BY g.group_id, g.group_name, g.group_comment, g.audit_info";
   }
 
   @Override
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
index 3783ef0495..de569eecff 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/h2/IdpUserMetaH2Provider.java
@@ -31,7 +31,7 @@ public class IdpUserMetaH2Provider extends 
IdpUserMetaBaseSQLProvider {
   @Override
   public String selectIdpUserWithGroups(@Param("username") String username) {
     return "SELECT u.user_name as name, u.password_hash as passwordHash,"
-        + " u.enabled as enabled,"
+        + " u.enabled as enabled, u.audit_info as auditInfo,"
         + " '['"
         + " || COALESCE(GROUP_CONCAT('\"' || g.group_name || '\"'), '')"
         + " || ']' as groupNames"
@@ -44,7 +44,7 @@ public class IdpUserMetaH2Provider extends 
IdpUserMetaBaseSQLProvider {
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
         + " g ON g.group_id = r.group_id AND g.deleted_at = 0"
         + " WHERE u.user_name = #{username} AND u.deleted_at = 0"
-        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled";
+        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled, 
u.audit_info";
   }
 
   @Override
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpGroupMetaPostgreSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpGroupMetaPostgreSQLProvider.java
index 21fea70a8d..a4af2bdd64 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpGroupMetaPostgreSQLProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpGroupMetaPostgreSQLProvider.java
@@ -32,6 +32,7 @@ public class IdpGroupMetaPostgreSQLProvider extends 
IdpGroupMetaBaseSQLProvider
   public String selectIdpGroupWithUsers(@Param("groupName") String groupName) {
     return "SELECT g.group_name as name,"
         + " COALESCE(g.group_comment, '') as comment,"
+        + " g.audit_info as auditInfo,"
         + " COALESCE(JSON_AGG(u.user_name), '[]'::json) as usernames"
         + " FROM "
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
@@ -42,7 +43,7 @@ public class IdpGroupMetaPostgreSQLProvider extends 
IdpGroupMetaBaseSQLProvider
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
         + " u ON u.user_id = r.user_id AND u.deleted_at = 0"
         + " WHERE g.group_name = #{groupName} AND g.deleted_at = 0"
-        + " GROUP BY g.group_id, g.group_name, g.group_comment";
+        + " GROUP BY g.group_id, g.group_name, g.group_comment, g.audit_info";
   }
 
   @Override
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
index 02fa785490..39975b6b84 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/postgresql/IdpUserMetaPostgreSQLProvider.java
@@ -31,7 +31,7 @@ public class IdpUserMetaPostgreSQLProvider extends 
IdpUserMetaBaseSQLProvider {
   @Override
   public String selectIdpUserWithGroups(@Param("username") String username) {
     return "SELECT u.user_name as name, u.password_hash as passwordHash,"
-        + " u.enabled as enabled,"
+        + " u.enabled as enabled, u.audit_info as auditInfo,"
         + " COALESCE(JSON_AGG(g.group_name), '[]'::json) as groupNames"
         + " FROM "
         + IdpUserMetaMapper.IDP_USER_TABLE_NAME
@@ -42,7 +42,7 @@ public class IdpUserMetaPostgreSQLProvider extends 
IdpUserMetaBaseSQLProvider {
         + IdpGroupMetaMapper.IDP_GROUP_TABLE_NAME
         + " g ON g.group_id = r.group_id AND g.deleted_at = 0"
         + " WHERE u.user_name = #{username} AND u.deleted_at = 0"
-        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled";
+        + " GROUP BY u.user_id, u.user_name, u.password_hash, u.enabled, 
u.audit_info";
   }
 
   @Override
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupPO.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupPO.java
index 6a53e2051c..4d64030ff2 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupPO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupPO.java
@@ -18,8 +18,8 @@
  */
 package org.apache.gravitino.idp.storage.po;
 
+import com.google.common.base.Preconditions;
 import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -30,13 +30,31 @@ import lombok.ToString;
 @EqualsAndHashCode
 @ToString
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
 @Builder(setterPrefix = "with")
 public class IdpGroupPO {
   private Long groupId;
   private String groupName;
   private String groupComment;
+  private String auditInfo;
   private Long currentVersion;
   private Long lastVersion;
   private Long deletedAt;
+
+  private IdpGroupPO(
+      Long groupId,
+      String groupName,
+      String groupComment,
+      String auditInfo,
+      Long currentVersion,
+      Long lastVersion,
+      Long deletedAt) {
+    Preconditions.checkArgument(auditInfo != null, "Audit info is required");
+    this.groupId = groupId;
+    this.groupName = groupName;
+    this.groupComment = groupComment;
+    this.auditInfo = auditInfo;
+    this.currentVersion = currentVersion;
+    this.lastVersion = lastVersion;
+    this.deletedAt = deletedAt;
+  }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupWithUsersPO.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupWithUsersPO.java
index 99306d8071..f77bcc0e66 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupWithUsersPO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpGroupWithUsersPO.java
@@ -39,5 +39,6 @@ import lombok.ToString;
 public class IdpGroupWithUsersPO {
   private String name;
   private String comment;
+  private String auditInfo;
   private String usernames;
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserGroupRelPO.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserGroupRelPO.java
index ecf168f9f7..d3f712fad6 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserGroupRelPO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserGroupRelPO.java
@@ -18,8 +18,8 @@
  */
 package org.apache.gravitino.idp.storage.po;
 
+import com.google.common.base.Preconditions;
 import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -30,13 +30,31 @@ import lombok.ToString;
 @EqualsAndHashCode
 @ToString
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
 @Builder(setterPrefix = "with")
 public class IdpUserGroupRelPO {
   private Long id;
   private Long userId;
   private Long groupId;
+  private String auditInfo;
   private Long currentVersion;
   private Long lastVersion;
   private Long deletedAt;
+
+  private IdpUserGroupRelPO(
+      Long id,
+      Long userId,
+      Long groupId,
+      String auditInfo,
+      Long currentVersion,
+      Long lastVersion,
+      Long deletedAt) {
+    Preconditions.checkArgument(auditInfo != null, "Audit info is required");
+    this.id = id;
+    this.userId = userId;
+    this.groupId = groupId;
+    this.auditInfo = auditInfo;
+    this.currentVersion = currentVersion;
+    this.lastVersion = lastVersion;
+    this.deletedAt = deletedAt;
+  }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
index 7dfed4b3fb..206d7e89f2 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserPO.java
@@ -18,8 +18,8 @@
  */
 package org.apache.gravitino.idp.storage.po;
 
+import com.google.common.base.Preconditions;
 import lombok.AccessLevel;
-import lombok.AllArgsConstructor;
 import lombok.Builder;
 import lombok.EqualsAndHashCode;
 import lombok.Getter;
@@ -30,14 +30,34 @@ import lombok.ToString;
 @EqualsAndHashCode
 @ToString
 @NoArgsConstructor(access = AccessLevel.PRIVATE)
-@AllArgsConstructor(access = AccessLevel.PRIVATE)
 @Builder(setterPrefix = "with")
 public class IdpUserPO {
   private Long userId;
   private String username;
   private String passwordHash;
   @Builder.Default private Boolean enabled = true;
+  private String auditInfo;
   private Long currentVersion;
   private Long lastVersion;
   private Long deletedAt;
+
+  private IdpUserPO(
+      Long userId,
+      String username,
+      String passwordHash,
+      Boolean enabled,
+      String auditInfo,
+      Long currentVersion,
+      Long lastVersion,
+      Long deletedAt) {
+    Preconditions.checkArgument(auditInfo != null, "Audit info is required");
+    this.userId = userId;
+    this.username = username;
+    this.passwordHash = passwordHash;
+    this.enabled = enabled == null ? Boolean.TRUE : enabled;
+    this.auditInfo = auditInfo;
+    this.currentVersion = currentVersion;
+    this.lastVersion = lastVersion;
+    this.deletedAt = deletedAt;
+  }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
index 04370011cf..115fa1ca4b 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/po/IdpUserWithGroupsPO.java
@@ -40,5 +40,6 @@ public class IdpUserWithGroupsPO {
   private String name;
   private String passwordHash;
   @Builder.Default private Boolean enabled = true;
+  private String auditInfo;
   private String groupNames;
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
index 61aa06bbed..feab658dbc 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/relational/utils/IdpPOConverters.java
@@ -18,7 +18,9 @@
  */
 package org.apache.gravitino.idp.storage.relational.utils;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.google.common.base.Preconditions;
+import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
 import java.util.stream.Collectors;
@@ -28,11 +30,76 @@ import org.apache.gravitino.idp.model.IdpUser;
 import org.apache.gravitino.idp.storage.po.IdpGroupWithUsersPO;
 import org.apache.gravitino.idp.storage.po.IdpUserWithGroupsPO;
 import org.apache.gravitino.json.JsonUtils;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.utils.PrincipalUtils;
 
 /** Converts built-in IdP persistence objects to domain models. */
 public final class IdpPOConverters {
+
   private IdpPOConverters() {}
 
+  /**
+   * Serializes audit information for persistence.
+   *
+   * @param auditInfo audit information
+   * @return JSON string
+   */
+  public static String toAuditInfoJson(AuditInfo auditInfo) {
+    Preconditions.checkNotNull(auditInfo, "auditInfo must not be null");
+    try {
+      return JsonUtils.anyFieldMapper().writeValueAsString(auditInfo);
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException("Failed to serialize IdP audit info", e);
+    }
+  }
+
+  /**
+   * Deserializes audit information from persistence.
+   *
+   * @param json JSON string, blank means empty audit
+   * @return audit information
+   */
+  public static AuditInfo fromAuditInfoJson(String json) {
+    if (StringUtils.isBlank(json)) {
+      return AuditInfo.EMPTY;
+    }
+    try {
+      return JsonUtils.anyFieldMapper().readValue(json, AuditInfo.class);
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException("Failed to deserialize IdP audit info", e);
+    }
+  }
+
+  /**
+   * Builds create-time audit JSON for the current principal.
+   *
+   * @return serialized audit info
+   */
+  public static String newCreateAuditInfoJson() {
+    return toAuditInfoJson(
+        AuditInfo.builder()
+            .withCreator(PrincipalUtils.getCurrentUserName())
+            .withCreateTime(Instant.now())
+            .build());
+  }
+
+  /**
+   * Rebuilds audit JSON with last-modifier fields for the current principal.
+   *
+   * @param existingAuditInfoJson previously stored audit JSON
+   * @return updated audit JSON
+   */
+  public static String withLastModifiedAuditInfoJson(String 
existingAuditInfoJson) {
+    AuditInfo existing = fromAuditInfoJson(existingAuditInfoJson);
+    return toAuditInfoJson(
+        AuditInfo.builder()
+            .withCreator(existing.creator())
+            .withCreateTime(existing.createTime())
+            .withLastModifier(PrincipalUtils.getCurrentUserName())
+            .withLastModifiedTime(Instant.now())
+            .build());
+  }
+
   /**
    * Converts a joined user row to a built-in IdP user.
    *
@@ -43,10 +110,11 @@ public final class IdpPOConverters {
     Preconditions.checkNotNull(userPO, "userPO must not be null");
     List<String> groupNames = parseJsonStringList(userPO.getGroupNames());
     boolean enabled = userPO.getEnabled() == null || userPO.getEnabled();
+    AuditInfo auditInfo = fromAuditInfoJson(userPO.getAuditInfo());
     if (StringUtils.isBlank(userPO.getPasswordHash())) {
-      return new IdpUser(userPO.getName(), groupNames, enabled);
+      return new IdpUser(userPO.getName(), groupNames, enabled, auditInfo);
     }
-    return new IdpUser(userPO.getName(), userPO.getPasswordHash(), groupNames, 
enabled);
+    return new IdpUser(userPO.getName(), userPO.getPasswordHash(), groupNames, 
enabled, auditInfo);
   }
 
   /**
@@ -58,7 +126,10 @@ public final class IdpPOConverters {
   public static IdpGroup fromIdpGroupWithUsersPO(IdpGroupWithUsersPO groupPO) {
     Preconditions.checkNotNull(groupPO, "groupPO must not be null");
     return new IdpGroup(
-        groupPO.getName(), parseJsonStringList(groupPO.getUsernames()), 
groupPO.getComment());
+        groupPO.getName(),
+        parseJsonStringList(groupPO.getUsernames()),
+        groupPO.getComment(),
+        fromAuditInfoJson(groupPO.getAuditInfo()));
   }
 
   @SuppressWarnings("unchecked")
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
index 018b20fe1a..f7f90adc2c 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpGroupMetaService.java
@@ -223,6 +223,7 @@ public class IdpGroupMetaService {
         .withId(RandomIdGenerator.INSTANCE.nextId())
         .withUserId(userId)
         .withGroupId(groupId)
+        .withAuditInfo(IdpPOConverters.newCreateAuditInfoJson())
         .withCurrentVersion(1L)
         .withLastVersion(0L)
         .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
index 79b8a43728..89f6ba6f19 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/service/IdpUserMetaService.java
@@ -134,7 +134,10 @@ public class IdpUserMetaService {
           if (userPO == null) {
             throw new NotFoundException("IdP user not found: %s", username);
           }
-          mapper.updateIdpUserPassword(username, passwordHash);
+          mapper.updateIdpUserPassword(
+              username,
+              passwordHash,
+              
IdpPOConverters.withLastModifiedAuditInfoJson(userPO.getAuditInfo()));
           return true;
         });
   }
@@ -159,7 +162,10 @@ public class IdpUserMetaService {
           if (userPO == null) {
             throw new NotFoundException("IdP user not found: %s", username);
           }
-          mapper.updateIdpUserEnabled(username, enabled);
+          mapper.updateIdpUserEnabled(
+              username,
+              enabled,
+              
IdpPOConverters.withLastModifiedAuditInfoJson(userPO.getAuditInfo()));
           return true;
         });
   }
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 95aaa1b9fd..b843e5670f 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
@@ -27,6 +27,7 @@ import org.apache.gravitino.UserPrincipal;
 import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.exceptions.AlreadyExistsException;
+import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.exceptions.NonEmptyEntityException;
 import org.apache.gravitino.exceptions.NotFoundException;
 import org.apache.gravitino.utils.PrincipalUtils;
@@ -116,6 +117,9 @@ public final class IdpRESTUtils {
     if (e instanceof IllegalArgumentException) {
       return illegalArguments(errorMsg, e);
     }
+    if (e instanceof ForbiddenException) {
+      return forbidden(errorMsg, e);
+    }
     if (e instanceof NotFoundException) {
       return notFound(errorMsg, e);
     }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpAuthorizationFilter.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpAuthorizationFilter.java
index 6bad34ea3f..24f8ff6f05 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpAuthorizationFilter.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpAuthorizationFilter.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.function.Supplier;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.container.ContainerRequestFilter;
+import javax.ws.rs.core.PathSegment;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.idp.web.IdpManagement;
@@ -32,9 +33,13 @@ import org.apache.gravitino.utils.PrincipalUtils;
 /**
  * Enforces built-in IdP management API access rules without server 
interception.
  *
- * <p>Caller must be listed in {@link Configs#SERVICE_ADMINS}. This filter 
runs as a Jersey request
- * filter after the servlet {@code AuthenticationFilter} has authenticated the 
caller and populated
- * the current user principal.
+ * <p>Callers listed in {@link Configs#SERVICE_ADMINS} may perform all IdP 
management operations.
+ * Authenticated non-admin users may {@code GET} or {@code PUT} {@code 
/idp/users/{user}} only for
+ * their own username (read profile / change password). Other IdP management 
APIs remain
+ * service-admin only.
+ *
+ * <p>This filter runs as a Jersey request filter after the servlet {@code 
AuthenticationFilter} has
+ * authenticated the caller and populated the current user principal.
  *
  * <p>Registered only when {@code basic} is configured in {@link 
Configs#AUTHENTICATORS}. Scoped to
  * resources annotated with {@link IdpManagement} via Jersey name binding.
@@ -42,7 +47,7 @@ import org.apache.gravitino.utils.PrincipalUtils;
 @IdpManagement
 public class IdpAuthorizationFilter implements ContainerRequestFilter {
 
-  /** Error message when the caller is not a service admin. */
+  /** Error message when the caller is not allowed to perform the IdP 
management operation. */
   public static final String SERVICE_ADMIN_REQUIRED_MESSAGE =
       "Only service admins can manage built-in IdP users and groups.";
 
@@ -64,12 +69,35 @@ public class IdpAuthorizationFilter implements 
ContainerRequestFilter {
 
   @Override
   public void filter(ContainerRequestContext requestContext) throws 
IOException {
-    if (!isServiceAdmin(serviceAdminsSupplier.get(), 
currentUserSupplier.get())) {
-      
requestContext.abortWith(IdpRESTUtils.forbidden(SERVICE_ADMIN_REQUIRED_MESSAGE, 
null));
+    String currentUser = currentUserSupplier.get();
+    if (isServiceAdmin(serviceAdminsSupplier.get(), currentUser)
+        || isSelfUserAccess(requestContext, currentUser)) {
+      return;
     }
+    
requestContext.abortWith(IdpRESTUtils.forbidden(SERVICE_ADMIN_REQUIRED_MESSAGE, 
null));
   }
 
   static boolean isServiceAdmin(List<String> serviceAdmins, String 
currentUser) {
     return currentUser != null && serviceAdmins != null && 
serviceAdmins.contains(currentUser);
   }
+
+  /**
+   * Returns whether the request is a GET or PUT to {@code 
/idp/users/{currentUser}}.
+   *
+   * <p>The resource method still rejects non-admin updates that change {@code 
enabled}.
+   */
+  static boolean isSelfUserAccess(ContainerRequestContext requestContext, 
String currentUser) {
+    if (currentUser == null) {
+      return false;
+    }
+    String method = requestContext.getMethod();
+    if (!"GET".equalsIgnoreCase(method) && !"PUT".equalsIgnoreCase(method)) {
+      return false;
+    }
+    List<PathSegment> segments = requestContext.getUriInfo().getPathSegments();
+    return segments.size() == 3
+        && "idp".equals(segments.get(0).getPath())
+        && "users".equals(segments.get(1).getPath())
+        && currentUser.equals(segments.get(2).getPath());
+  }
 }
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 31f7349807..5db95b4f20 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
@@ -20,6 +20,7 @@ package org.apache.gravitino.idp.web.rest;
 
 import com.codahale.metrics.annotation.ResponseMetered;
 import com.codahale.metrics.annotation.Timed;
+import java.util.List;
 import javax.inject.Inject;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.DELETE;
@@ -31,7 +32,10 @@ import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
+import org.apache.gravitino.Configs;
+import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.dto.responses.RemoveResponse;
+import org.apache.gravitino.exceptions.ForbiddenException;
 import org.apache.gravitino.idp.IdpUserGroupManager;
 import org.apache.gravitino.idp.dto.requests.AddUserRequest;
 import org.apache.gravitino.idp.dto.requests.UpdateUserRequest;
@@ -40,6 +44,7 @@ import org.apache.gravitino.idp.web.IdpManagement;
 import org.apache.gravitino.idp.web.IdpOperationType;
 import org.apache.gravitino.idp.web.IdpRESTUtils;
 import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.utils.PrincipalUtils;
 
 /** REST resource for built-in IdP user management exposed by the {@code 
idp-basic} plugin. */
 @IdpManagement
@@ -99,6 +104,7 @@ public class IdpUserOperations {
         httpRequest,
         () -> {
           request.validate();
+          enforceSelfPasswordUpdateRules(user, request);
           if (request.getPassword() != null) {
             userGroupManager.changePassword(user, request.getPassword());
           }
@@ -112,6 +118,27 @@ public class IdpUserOperations {
         user);
   }
 
+  /**
+   * Non-service-admins may only change their own password and cannot update 
{@code enabled}.
+   *
+   * @param user the path username being updated
+   * @param request the update request
+   */
+  private static void enforceSelfPasswordUpdateRules(String user, 
UpdateUserRequest request) {
+    String currentUser = PrincipalUtils.getCurrentUserName();
+    List<String> serviceAdmins = 
GravitinoEnv.getInstance().config().get(Configs.SERVICE_ADMINS);
+    if (IdpAuthorizationFilter.isServiceAdmin(serviceAdmins, currentUser)) {
+      return;
+    }
+    if (!user.equals(currentUser)) {
+      throw new ForbiddenException(
+          "Only service admins can update another user's password or enabled 
flag");
+    }
+    if (request.getEnabled() != null) {
+      throw new ForbiddenException("Only service admins can update the enabled 
flag");
+    }
+  }
+
   @DELETE
   @Path("{user}")
   @Produces("application/vnd.gravitino.v1+json")
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
index 0a7a835654..d3d017bc96 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/TestIdpUserGroupManager.java
@@ -38,6 +38,7 @@ import java.nio.file.Path;
 import java.util.Comparator;
 import java.util.stream.Stream;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.auth.AuthenticatorType;
 import org.apache.gravitino.exceptions.AlreadyExistsException;
 import org.apache.gravitino.exceptions.NonEmptyEntityException;
@@ -100,10 +101,14 @@ public class TestIdpUserGroupManager {
     Assertions.assertEquals("testAdd", user.name());
     Assertions.assertTrue(user.enabled());
     Assertions.assertTrue(user.groupNames().isEmpty());
+    Assertions.assertEquals(AuthConstants.ANONYMOUS_USER, 
user.auditInfo().creator());
+    Assertions.assertNotNull(user.auditInfo().createTime());
+    Assertions.assertNull(user.auditInfo().lastModifier());
 
     IdpUser disabled = manager.addUser("testAddDisabled", VALID_PASSWORD, 
false);
     Assertions.assertFalse(disabled.enabled());
     Assertions.assertFalse(manager.getUser("testAddDisabled").enabled());
+    Assertions.assertEquals(AuthConstants.ANONYMOUS_USER, 
disabled.auditInfo().creator());
 
     Assertions.assertThrows(
         AlreadyExistsException.class, () -> manager.addUser("testAdd", 
ANOTHER_VALID_PASSWORD));
@@ -115,6 +120,8 @@ public class TestIdpUserGroupManager {
 
     IdpUser user = manager.getUser("testGet");
     Assertions.assertEquals("testGet", user.name());
+    Assertions.assertEquals(AuthConstants.ANONYMOUS_USER, 
user.auditInfo().creator());
+    Assertions.assertNotNull(user.auditInfo().createTime());
 
     Throwable exception =
         Assertions.assertThrows(NotFoundException.class, () -> 
manager.getUser("not-exist"));
@@ -134,7 +141,11 @@ public class TestIdpUserGroupManager {
     manager.addUser("testChangePassword", VALID_PASSWORD);
 
     Assertions.assertTrue(manager.changePassword("testChangePassword", 
NEW_VALID_PASSWORD));
-    Assertions.assertEquals("testChangePassword", 
manager.getUser("testChangePassword").name());
+    IdpUser updated = manager.getUser("testChangePassword");
+    Assertions.assertEquals("testChangePassword", updated.name());
+    Assertions.assertEquals(AuthConstants.ANONYMOUS_USER, 
updated.auditInfo().creator());
+    Assertions.assertEquals(AuthConstants.ANONYMOUS_USER, 
updated.auditInfo().lastModifier());
+    Assertions.assertNotNull(updated.auditInfo().lastModifiedTime());
 
     Assertions.assertThrows(
         NotFoundException.class, () -> manager.changePassword("not-exist", 
VALID_PASSWORD));
@@ -148,7 +159,10 @@ public class TestIdpUserGroupManager {
         "testEnabled", manager.authenticate("testEnabled", 
VALID_PASSWORD).name());
 
     Assertions.assertTrue(manager.updateEnabled("testEnabled", false));
-    Assertions.assertFalse(manager.getUser("testEnabled").enabled());
+    IdpUser disabled = manager.getUser("testEnabled");
+    Assertions.assertFalse(disabled.enabled());
+    Assertions.assertEquals(AuthConstants.ANONYMOUS_USER, 
disabled.auditInfo().lastModifier());
+    Assertions.assertNotNull(disabled.auditInfo().lastModifiedTime());
     Assertions.assertThrows(
         UnauthorizedException.class, () -> manager.authenticate("testEnabled", 
VALID_PASSWORD));
 
@@ -192,6 +206,9 @@ public class TestIdpUserGroupManager {
     Assertions.assertEquals("testAddGroup", group.name());
     Assertions.assertEquals("", group.comment());
     Assertions.assertTrue(group.usernames().isEmpty());
+    Assertions.assertEquals(AuthConstants.ANONYMOUS_USER, 
group.auditInfo().creator());
+    Assertions.assertNotNull(group.auditInfo().createTime());
+    Assertions.assertNull(group.auditInfo().lastModifier());
 
     IdpGroup commented = manager.addGroup("testAddGroupComment", "on-call 
rotation");
     Assertions.assertEquals("on-call rotation", commented.comment());
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpGroupDTO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpGroupDTO.java
index 1bdc6bbbc0..c8a995a33a 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpGroupDTO.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpGroupDTO.java
@@ -20,8 +20,10 @@
 package org.apache.gravitino.idp.dto;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
+import org.apache.gravitino.dto.AuditDTO;
 import org.apache.gravitino.json.JsonUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -30,10 +32,13 @@ public class TestIdpGroupDTO {
 
   @Test
   public void testIdpGroupDTOSerDe() throws JsonProcessingException {
+    AuditDTO audit =
+        
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.EPOCH).build();
     IdpGroupDTO groupDTO =
         IdpGroupDTO.builder()
             .withName("test_group")
             .withUsers(Arrays.asList("user1", "user2"))
+            .withAudit(audit)
             .build();
 
     String json = JsonUtils.objectMapper().writeValueAsString(groupDTO);
@@ -42,6 +47,7 @@ public class TestIdpGroupDTO {
     Assertions.assertEquals(groupDTO, deserialized);
     Assertions.assertEquals("test_group", deserialized.name());
     Assertions.assertEquals(Arrays.asList("user1", "user2"), 
deserialized.users());
+    Assertions.assertEquals(audit, deserialized.audit());
 
     // Test with default users
     IdpGroupDTO groupDTO1 = 
IdpGroupDTO.builder().withName("test_group").build();
@@ -53,8 +59,9 @@ public class TestIdpGroupDTO {
     Assertions.assertEquals("test_group", deserialized1.name());
     Assertions.assertTrue(deserialized1.users().isEmpty());
     Assertions.assertEquals("", deserialized1.comment());
+    Assertions.assertNull(deserialized1.audit());
     Assertions.assertEquals(
-        "IdpGroupDTO(name=test_group, comment=, users=[])", 
deserialized1.toString());
+        "IdpGroupDTO(name=test_group, comment=, users=[], audit=null)", 
deserialized1.toString());
 
     IdpGroupDTO deserializedWithNullUsers =
         JsonUtils.objectMapper()
@@ -62,7 +69,8 @@ public class TestIdpGroupDTO {
     Assertions.assertTrue(deserializedWithNullUsers.users().isEmpty());
     Assertions.assertEquals("", deserializedWithNullUsers.comment());
     Assertions.assertEquals(
-        "IdpGroupDTO(name=test_group, comment=, users=[])", 
deserializedWithNullUsers.toString());
+        "IdpGroupDTO(name=test_group, comment=, users=[], audit=null)",
+        deserializedWithNullUsers.toString());
 
     IdpGroupDTO deserializedNullComment =
         JsonUtils.objectMapper()
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
index 291f1ece36..37aa61e50b 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/dto/TestIdpUserDTO.java
@@ -20,8 +20,10 @@
 package org.apache.gravitino.idp.dto;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
+import java.time.Instant;
 import java.util.Arrays;
 import java.util.Collections;
+import org.apache.gravitino.dto.AuditDTO;
 import org.apache.gravitino.json.JsonUtils;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -30,10 +32,13 @@ public class TestIdpUserDTO {
 
   @Test
   public void testIdpUserDTOSerDe() throws JsonProcessingException {
+    AuditDTO audit =
+        
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.EPOCH).build();
     IdpUserDTO userDTO =
         IdpUserDTO.builder()
             .withName("test_user")
             .withGroups(Arrays.asList("group1", "group2"))
+            .withAudit(audit)
             .build();
 
     String json = JsonUtils.objectMapper().writeValueAsString(userDTO);
@@ -42,6 +47,7 @@ public class TestIdpUserDTO {
     Assertions.assertEquals(userDTO, deserialized);
     Assertions.assertEquals("test_user", deserialized.name());
     Assertions.assertEquals(Arrays.asList("group1", "group2"), 
deserialized.groups());
+    Assertions.assertEquals(audit, deserialized.audit());
 
     // Test with default groups
     IdpUserDTO userDTO1 = IdpUserDTO.builder().withName("test_user").build();
@@ -52,15 +58,17 @@ public class TestIdpUserDTO {
     Assertions.assertEquals(userDTO1, deserialized1);
     Assertions.assertEquals("test_user", deserialized1.name());
     Assertions.assertTrue(deserialized1.groups().isEmpty());
+    Assertions.assertNull(deserialized1.audit());
     Assertions.assertEquals(
-        "IdpUserDTO(name=test_user, enabled=true, groups=[])", 
deserialized1.toString());
+        "IdpUserDTO(name=test_user, enabled=true, groups=[], audit=null)",
+        deserialized1.toString());
 
     IdpUserDTO deserializedWithNullGroups =
         JsonUtils.objectMapper()
             .readValue("{\"name\":\"test_user\",\"groups\":null}", 
IdpUserDTO.class);
     Assertions.assertTrue(deserializedWithNullGroups.groups().isEmpty());
     Assertions.assertEquals(
-        "IdpUserDTO(name=test_user, enabled=true, groups=[])",
+        "IdpUserDTO(name=test_user, enabled=true, groups=[], audit=null)",
         deserializedWithNullGroups.toString());
 
     Assertions.assertThrows(
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
index bf970fe1af..528d34da16 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRESTApiIT.java
@@ -132,9 +132,46 @@ public class IdpRESTApiIT extends BaseIT {
     // No Authorization: OAuth rejects the request before the IdP filter runs.
     Assertions.assertEquals(401, get("/idp/users/" + USER1, null, 
null).statusCode());
 
+    postUser(USER1, USER_PASSWORD);
     postUser(USER2, USER_PASSWORD);
+    Assertions.assertEquals(200, get("/idp/users/" + USER2, USER2, 
USER_PASSWORD).statusCode());
     assertError(
-        403, get("/idp/users/" + USER2, USER2, USER_PASSWORD), 
ErrorConstants.FORBIDDEN_CODE);
+        403, get("/idp/users/" + USER1, USER2, USER_PASSWORD), 
ErrorConstants.FORBIDDEN_CODE);
+    Assertions.assertTrue(deleteUser(USER1));
+    Assertions.assertTrue(deleteUser(USER2));
+  }
+
+  @Test
+  void testSelfUserAccess() throws Exception {
+    postUser(USER1, USER_PASSWORD);
+    postUser(USER2, USER_PASSWORD);
+
+    HttpResponse<String> selfGet = get("/idp/users/" + USER1, USER1, 
USER_PASSWORD);
+    Assertions.assertEquals(200, selfGet.statusCode(), selfGet.body());
+    IdpUserResponse self =
+        JsonUtils.objectMapper().readValue(selfGet.body(), 
IdpUserResponse.class);
+    self.validate();
+    Assertions.assertEquals(USER1, self.getUser().name());
+
+    HttpResponse<String> selfUpdate =
+        put("/idp/users/" + USER1, new UpdateUserRequest(UPDATED_PASSWORD), 
USER1, USER_PASSWORD);
+    Assertions.assertEquals(200, selfUpdate.statusCode(), selfUpdate.body());
+    Assertions.assertEquals(200, get("/version", USER1, 
UPDATED_PASSWORD).statusCode());
+
+    assertError(
+        403,
+        put(
+            "/idp/users/" + USER2,
+            new UpdateUserRequest(UPDATED_PASSWORD),
+            USER1,
+            UPDATED_PASSWORD),
+        ErrorConstants.FORBIDDEN_CODE);
+    assertError(
+        403,
+        put("/idp/users/" + USER1, new UpdateUserRequest(null, false), USER1, 
UPDATED_PASSWORD),
+        ErrorConstants.FORBIDDEN_CODE);
+
+    Assertions.assertTrue(deleteUser(USER1));
     Assertions.assertTrue(deleteUser(USER2));
   }
 
@@ -381,8 +418,13 @@ public class IdpRESTApiIT extends BaseIT {
   }
 
   private static HttpResponse<String> put(String path, Object body) throws 
Exception {
+    return put(path, body, ADMIN, ADMIN_PASSWORD);
+  }
+
+  private static HttpResponse<String> put(
+      String path, Object body, String username, String password) throws 
Exception {
     return HTTP.send(
-        authorized(ADMIN, ADMIN_PASSWORD)
+        authorized(username, password)
             .uri(URI.create(apiBase + path))
             .header("Content-Type", ACCEPT)
             .PUT(jsonBody(body))
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpGroupMetaStorage.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpGroupMetaStorage.java
index c4be1c4e9a..cc9f46d995 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpGroupMetaStorage.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpGroupMetaStorage.java
@@ -50,6 +50,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withGroupId(1L)
             .withGroupName("dev")
             .withGroupComment("")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -69,6 +70,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withGroupId(1L)
             .withGroupName("dev")
             .withGroupComment("on-call rotation")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -89,6 +91,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withGroupId(1L)
             .withGroupName("dev")
             .withGroupComment("")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -101,6 +104,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -110,6 +114,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(2L)
             .withUsername("bob")
             .withPasswordHash("hash-b")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -120,6 +125,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(100L)
                 .withUserId(1L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -128,6 +134,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(101L)
                 .withUserId(2L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -150,6 +157,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withGroupId(1L)
             .withGroupName("dev")
             .withGroupComment("")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -159,6 +167,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(2L)
             .withGroupName("ops")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(10L)
@@ -177,6 +186,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withGroupId(1L)
             .withGroupName("dev")
             .withGroupComment("")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -196,6 +206,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("legacy-group")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(10L)
@@ -204,6 +215,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(2L)
             .withGroupName("new-group")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(30L)
@@ -212,6 +224,7 @@ class TestIdpGroupMetaStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(3L)
             .withGroupName("active-group")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserGroupRelStorage.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserGroupRelStorage.java
index 80d0a47ede..86c6d0d772 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserGroupRelStorage.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserGroupRelStorage.java
@@ -52,6 +52,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("dev")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -60,6 +61,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(2L)
             .withGroupName("ops")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -69,6 +71,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -79,6 +82,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(100L)
                 .withUserId(1L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -87,6 +91,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(101L)
                 .withUserId(1L)
                 .withGroupId(2L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -106,6 +111,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("dev")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -115,6 +121,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -124,6 +131,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(2L)
             .withUsername("bob")
             .withPasswordHash("hash-b")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -134,6 +142,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(100L)
                 .withUserId(1L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -142,6 +151,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(101L)
                 .withUserId(2L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -161,6 +171,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("dev")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -170,6 +181,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -179,6 +191,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(2L)
             .withUsername("bob")
             .withPasswordHash("hash-b")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -189,6 +202,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(100L)
                 .withUserId(1L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -197,6 +211,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(101L)
                 .withUserId(2L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -219,6 +234,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("dev")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -228,6 +244,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -237,6 +254,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(2L)
             .withUsername("bob")
             .withPasswordHash("hash-b")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -247,6 +265,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(100L)
                 .withUserId(1L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -255,6 +274,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(101L)
                 .withUserId(2L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -273,6 +293,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(2L)
             .withGroupName("ops")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -282,6 +303,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(3L)
             .withUsername("carol")
             .withPasswordHash("hash-c")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -292,6 +314,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(102L)
                 .withUserId(3L)
                 .withGroupId(2L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -310,6 +333,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("dev")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -318,6 +342,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(2L)
             .withGroupName("ops")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -326,6 +351,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(3L)
             .withGroupName("qa")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -335,6 +361,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("legacy-user")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -344,6 +371,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(2L)
             .withUsername("new-user")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -353,6 +381,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(3L)
             .withUsername("active-user")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -363,6 +392,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(100L)
                 .withUserId(1L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(10L)
@@ -373,6 +403,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(101L)
                 .withUserId(2L)
                 .withGroupId(2L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(30L)
@@ -383,6 +414,7 @@ class TestIdpUserGroupRelStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(102L)
                 .withUserId(3L)
                 .withGroupId(3L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
index c8a5b9701c..4c13effd8f 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/mapper/TestIdpUserMetaStorage.java
@@ -50,6 +50,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -69,6 +70,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -80,6 +82,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("dev")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -88,6 +91,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
         IdpGroupPO.builder()
             .withGroupId(2L)
             .withGroupName("ops")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -98,6 +102,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(100L)
                 .withUserId(1L)
                 .withGroupId(1L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -106,6 +111,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
                 .withId(101L)
                 .withUserId(1L)
                 .withGroupId(2L)
+                .withAuditInfo("{}")
                 .withCurrentVersion(1L)
                 .withLastVersion(0L)
                 .withDeletedAt(0L)
@@ -128,6 +134,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -137,6 +144,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(2L)
             .withUsername("bob")
             .withPasswordHash("hash-b")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -160,12 +168,13 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
             .build();
     idpUserMetaMapper.insertIdpUser(oldUserPO);
-    assertEquals(1, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2"));
+    assertEquals(1, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2", "{}"));
     assertEquals("hash-a-2", 
idpUserMetaMapper.selectIdpUser("alice").getPasswordHash());
     assertEquals(1L, 
idpUserMetaMapper.selectIdpUser("alice").getCurrentVersion());
     assertEquals(0L, 
idpUserMetaMapper.selectIdpUser("alice").getLastVersion());
@@ -180,16 +189,17 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(3L)
             .withLastVersion(2L)
             .withDeletedAt(0L)
             .build());
 
-    assertEquals(1, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2"));
+    assertEquals(1, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2", "{}"));
     assertEquals("hash-a-2", 
idpUserMetaMapper.selectIdpUser("alice").getPasswordHash());
     assertEquals(3L, 
idpUserMetaMapper.selectIdpUser("alice").getCurrentVersion());
     assertEquals(2L, 
idpUserMetaMapper.selectIdpUser("alice").getLastVersion());
-    assertEquals(1, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2"));
+    assertEquals(1, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2", "{}"));
   }
 
   @ParameterizedTest
@@ -201,6 +211,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash-a")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -210,7 +221,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
     assertEquals(1, idpUserMetaMapper.softDeleteIdpUser("alice"));
     assertNull(idpUserMetaMapper.selectIdpUser("alice"));
     assertEquals(0, idpUserMetaMapper.softDeleteIdpUser("alice"));
-    assertEquals(0, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2"));
+    assertEquals(0, idpUserMetaMapper.updateIdpUserPassword("alice", 
"hash-a-2", "{}"));
     assertEquals(1, 
idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(Long.MAX_VALUE, 10));
     assertEquals(0, 
idpUserMetaMapper.deleteIdpUserMetasByLegacyTimeline(Long.MAX_VALUE, 10));
   }
@@ -224,6 +235,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(1L)
             .withUsername("legacy-user")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(10L)
@@ -233,6 +245,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(2L)
             .withUsername("new-user")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(30L)
@@ -242,6 +255,7 @@ class TestIdpUserMetaStorage extends 
AbstractIdpMetaStorageTest {
             .withUserId(3L)
             .withUsername("active-user")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpGroupPO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpGroupPO.java
index a10ba8db8d..20ff3a71d8 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpGroupPO.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpGroupPO.java
@@ -30,6 +30,7 @@ public class TestIdpGroupPO {
             .withGroupId(1L)
             .withGroupName("engineering")
             .withGroupComment("platform engineering")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L)
@@ -37,12 +38,28 @@ public class TestIdpGroupPO {
 
     Assertions.assertEquals(1L, groupPO.getGroupId());
     Assertions.assertEquals("engineering", groupPO.getGroupName());
+    Assertions.assertEquals("{}", groupPO.getAuditInfo());
     Assertions.assertEquals("platform engineering", groupPO.getGroupComment());
     Assertions.assertEquals(1L, groupPO.getCurrentVersion());
     Assertions.assertEquals(1L, groupPO.getLastVersion());
     Assertions.assertEquals(0L, groupPO.getDeletedAt());
   }
 
+  @Test
+  public void testIdpGroupPOBuilderRequiresAuditInfo() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            IdpGroupPO.builder()
+                .withGroupId(1L)
+                .withGroupName("engineering")
+                .withGroupComment("platform engineering")
+                .withCurrentVersion(1L)
+                .withLastVersion(1L)
+                .withDeletedAt(0L)
+                .build());
+  }
+
   @Test
   public void testEqualsAndHashCode() {
     IdpGroupPO groupPO1 =
@@ -50,6 +67,7 @@ public class TestIdpGroupPO {
             .withGroupId(1L)
             .withGroupName("engineering")
             .withGroupComment("platform engineering")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L)
@@ -60,6 +78,7 @@ public class TestIdpGroupPO {
             .withGroupId(1L)
             .withGroupName("engineering")
             .withGroupComment("platform engineering")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L)
@@ -76,6 +95,7 @@ public class TestIdpGroupPO {
             .withGroupId(1L)
             .withGroupName("engineering")
             .withGroupComment("platform engineering")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L);
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserGroupRelPO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserGroupRelPO.java
index 4a5c34c1a7..2a205090a4 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserGroupRelPO.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserGroupRelPO.java
@@ -30,6 +30,7 @@ public class TestIdpUserGroupRelPO {
             .withId(1L)
             .withUserId(20L)
             .withGroupId(10L)
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -38,11 +39,27 @@ public class TestIdpUserGroupRelPO {
     Assertions.assertEquals(1L, relPO.getId());
     Assertions.assertEquals(20L, relPO.getUserId());
     Assertions.assertEquals(10L, relPO.getGroupId());
+    Assertions.assertEquals("{}", relPO.getAuditInfo());
     Assertions.assertEquals(1L, relPO.getCurrentVersion());
     Assertions.assertEquals(0L, relPO.getLastVersion());
     Assertions.assertEquals(0L, relPO.getDeletedAt());
   }
 
+  @Test
+  public void testIdpUserGroupRelPOBuilderRequiresAuditInfo() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            IdpUserGroupRelPO.builder()
+                .withId(1L)
+                .withUserId(20L)
+                .withGroupId(10L)
+                .withCurrentVersion(1L)
+                .withLastVersion(0L)
+                .withDeletedAt(0L)
+                .build());
+  }
+
   @Test
   public void testEqualsAndHashCode() {
     IdpUserGroupRelPO relPO1 =
@@ -50,6 +67,7 @@ public class TestIdpUserGroupRelPO {
             .withId(1L)
             .withUserId(20L)
             .withGroupId(10L)
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -60,6 +78,7 @@ public class TestIdpUserGroupRelPO {
             .withId(1L)
             .withUserId(20L)
             .withGroupId(10L)
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -76,6 +95,7 @@ public class TestIdpUserGroupRelPO {
             .withId(1L)
             .withUserId(20L)
             .withGroupId(10L)
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L);
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
index e4f00ce74a..751bfacaaf 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/po/TestIdpUserPO.java
@@ -30,6 +30,7 @@ public class TestIdpUserPO {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L)
@@ -39,17 +40,34 @@ public class TestIdpUserPO {
     Assertions.assertEquals("alice", userPO.getUsername());
     Assertions.assertEquals("hash", userPO.getPasswordHash());
     Assertions.assertTrue(userPO.getEnabled());
+    Assertions.assertEquals("{}", userPO.getAuditInfo());
     Assertions.assertEquals(1L, userPO.getCurrentVersion());
     Assertions.assertEquals(1L, userPO.getLastVersion());
     Assertions.assertEquals(0L, userPO.getDeletedAt());
   }
 
+  @Test
+  public void testIdpUserPOBuilderRequiresAuditInfo() {
+    Assertions.assertThrows(
+        IllegalArgumentException.class,
+        () ->
+            IdpUserPO.builder()
+                .withUserId(1L)
+                .withUsername("alice")
+                .withPasswordHash("hash")
+                .withCurrentVersion(1L)
+                .withLastVersion(1L)
+                .withDeletedAt(0L)
+                .build());
+  }
+
   @Test
   public void testIdpUserPOBuilderAllowsPartialFields() {
     IdpUserPO userPO =
         IdpUserPO.builder()
             .withUserId(1L)
             .withUsername("alice")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L)
@@ -67,6 +85,7 @@ public class TestIdpUserPO {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L)
@@ -77,6 +96,7 @@ public class TestIdpUserPO {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L)
@@ -93,6 +113,7 @@ public class TestIdpUserPO {
             .withUserId(1L)
             .withUsername("alice")
             .withPasswordHash("hash")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(1L)
             .withDeletedAt(0L);
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/relational/TestIdpGarbageCollector.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/relational/TestIdpGarbageCollector.java
index 7120c38d53..3969a500a1 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/relational/TestIdpGarbageCollector.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/relational/TestIdpGarbageCollector.java
@@ -92,6 +92,7 @@ class TestIdpGarbageCollector extends 
AbstractIdpMetaStorageTest {
           IdpGroupPO.builder()
               .withGroupId(index)
               .withGroupName("group" + index)
+              .withAuditInfo("{}")
               .withCurrentVersion(1L)
               .withLastVersion(0L)
               .withDeletedAt(0L)
@@ -106,6 +107,7 @@ class TestIdpGarbageCollector extends 
AbstractIdpMetaStorageTest {
               .withUserId(index)
               .withUsername("user" + index)
               .withPasswordHash("hash-" + index)
+              .withAuditInfo("{}")
               .withCurrentVersion(1L)
               .withLastVersion(0L)
               .withDeletedAt(0L)
@@ -133,6 +135,7 @@ class TestIdpGarbageCollector extends 
AbstractIdpMetaStorageTest {
         .withId(relationOrdinal)
         .withUserId(user.getUserId())
         .withGroupId(group.getGroupId())
+        .withAuditInfo("{}")
         .withCurrentVersion(1L)
         .withLastVersion(0L)
         .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/AbstractIdpMetaServiceTest.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/AbstractIdpMetaServiceTest.java
index f640f3ec28..344c63e148 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/AbstractIdpMetaServiceTest.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/AbstractIdpMetaServiceTest.java
@@ -90,6 +90,7 @@ public abstract class AbstractIdpMetaServiceTest extends 
AbstractIdpMetaStorageT
           IdpGroupPO.builder()
               .withGroupId(index)
               .withGroupName("group" + index)
+              .withAuditInfo("{}")
               .withCurrentVersion(1L)
               .withLastVersion(0L)
               .withDeletedAt(0L)
@@ -104,6 +105,7 @@ public abstract class AbstractIdpMetaServiceTest extends 
AbstractIdpMetaStorageT
               .withUserId(index)
               .withUsername("user" + index)
               .withPasswordHash("hash-" + index)
+              .withAuditInfo("{}")
               .withCurrentVersion(1L)
               .withLastVersion(0L)
               .withDeletedAt(0L)
@@ -145,6 +147,7 @@ public abstract class AbstractIdpMetaServiceTest extends 
AbstractIdpMetaStorageT
         .withId(relationOrdinal)
         .withUserId(user.getUserId())
         .withGroupId(group.getGroupId())
+        .withAuditInfo("{}")
         .withCurrentVersion(1L)
         .withLastVersion(0L)
         .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
index 70067d66ac..4922ffd357 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpGroupMetaService.java
@@ -61,6 +61,7 @@ class TestIdpGroupMetaService extends 
AbstractIdpMetaServiceTest {
                 IdpGroupPO.builder()
                     .withGroupId(1L)
                     .withGroupName("engineering")
+                    .withAuditInfo("{}")
                     .withCurrentVersion(1L)
                     .withLastVersion(0L)
                     .withDeletedAt(0L)
@@ -88,6 +89,7 @@ class TestIdpGroupMetaService extends 
AbstractIdpMetaServiceTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("group1")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -104,6 +106,7 @@ class TestIdpGroupMetaService extends 
AbstractIdpMetaServiceTest {
         IdpGroupPO.builder()
             .withGroupId(2L)
             .withGroupName("group1")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -143,6 +146,7 @@ class TestIdpGroupMetaService extends 
AbstractIdpMetaServiceTest {
         IdpGroupPO.builder()
             .withGroupId(1L)
             .withGroupName("engineering")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -174,6 +178,7 @@ class TestIdpGroupMetaService extends 
AbstractIdpMetaServiceTest {
                 IdpGroupPO.builder()
                     .withGroupId(1L)
                     .withGroupName("engineering")
+                    .withAuditInfo("{}")
                     .withCurrentVersion(1L)
                     .withLastVersion(0L)
                     .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
index fc1092f296..dcafe50558 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/storage/service/TestIdpUserMetaService.java
@@ -63,6 +63,7 @@ class TestIdpUserMetaService extends 
AbstractIdpMetaServiceTest {
             .withUserId(1L)
             .withUsername("user1")
             .withPasswordHash("hash-1")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
@@ -80,6 +81,7 @@ class TestIdpUserMetaService extends 
AbstractIdpMetaServiceTest {
             .withUserId(2L)
             .withUsername("user1")
             .withPasswordHash("hash-2")
+            .withAuditInfo("{}")
             .withCurrentVersion(1L)
             .withLastVersion(0L)
             .withDeletedAt(0L)
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpAuthorizationFilter.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpAuthorizationFilter.java
index 5fb8167357..2c4a017f83 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpAuthorizationFilter.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/web/rest/TestIdpAuthorizationFilter.java
@@ -20,8 +20,14 @@ package org.apache.gravitino.idp.web.rest;
 
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
+import java.util.Arrays;
 import java.util.List;
+import javax.ws.rs.container.ContainerRequestContext;
+import javax.ws.rs.core.PathSegment;
+import javax.ws.rs.core.UriInfo;
 import org.junit.jupiter.api.Test;
 
 class TestIdpAuthorizationFilter {
@@ -33,4 +39,40 @@ class TestIdpAuthorizationFilter {
     assertFalse(IdpAuthorizationFilter.isServiceAdmin(List.of("admin"), 
"other"));
     assertTrue(IdpAuthorizationFilter.isServiceAdmin(List.of("admin", "ops"), 
"admin"));
   }
+
+  @Test
+  void testIsSelfUserAccess() {
+    assertFalse(
+        IdpAuthorizationFilter.isSelfUserAccess(request("PUT", "idp", "users", 
"alice"), null));
+    assertFalse(
+        IdpAuthorizationFilter.isSelfUserAccess(
+            request("DELETE", "idp", "users", "alice"), "alice"));
+    assertFalse(
+        IdpAuthorizationFilter.isSelfUserAccess(request("PUT", "idp", "users", 
"bob"), "alice"));
+    assertFalse(
+        IdpAuthorizationFilter.isSelfUserAccess(request("GET", "idp", "users", 
"bob"), "alice"));
+    assertFalse(
+        IdpAuthorizationFilter.isSelfUserAccess(request("PUT", "idp", 
"groups", "alice"), "alice"));
+    assertTrue(
+        IdpAuthorizationFilter.isSelfUserAccess(request("GET", "idp", "users", 
"alice"), "alice"));
+    assertTrue(
+        IdpAuthorizationFilter.isSelfUserAccess(request("PUT", "idp", "users", 
"alice"), "alice"));
+  }
+
+  private static ContainerRequestContext request(String method, String... 
segments) {
+    List<PathSegment> pathSegments =
+        
Arrays.stream(segments).map(TestIdpAuthorizationFilter::segment).toList();
+    ContainerRequestContext context = mock(ContainerRequestContext.class);
+    UriInfo uriInfo = mock(UriInfo.class);
+    when(context.getMethod()).thenReturn(method);
+    when(context.getUriInfo()).thenReturn(uriInfo);
+    when(uriInfo.getPathSegments()).thenReturn(pathSegments);
+    return context;
+  }
+
+  private static PathSegment segment(String path) {
+    PathSegment pathSegment = mock(PathSegment.class);
+    when(pathSegment.getPath()).thenReturn(path);
+    return pathSegment;
+  }
 }
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 0bc2aa96b2..153d5908ac 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
@@ -36,6 +36,12 @@ import javax.ws.rs.client.Invocation;
 import javax.ws.rs.core.Application;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Config;
+import org.apache.gravitino.Configs;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.dto.responses.ErrorConstants;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.dto.responses.RemoveResponse;
@@ -56,7 +62,9 @@ import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.JerseyTest;
 import org.glassfish.jersey.test.TestProperties;
+import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 
@@ -66,6 +74,21 @@ class TestIdpOperations extends JerseyTest {
   private static final String VALID_PASSWORD = "Passw0rd-For-User";
   private static final IdpUserGroupManager MANAGER = 
mock(IdpUserGroupManager.class);
 
+  private static Config previousConfig;
+
+  @BeforeAll
+  static void installServiceAdminConfig() throws Exception {
+    previousConfig = GravitinoEnv.getInstance().config();
+    Config config = mock(Config.class);
+    when(config.get(Configs.SERVICE_ADMINS)).thenReturn(List.of("admin"));
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "config", config, true);
+  }
+
+  @AfterAll
+  static void restoreConfig() throws Exception {
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "config", 
previousConfig, true);
+  }
+
   @BeforeEach
   void resetManager() {
     reset(MANAGER);
@@ -82,6 +105,8 @@ class TestIdpOperations extends JerseyTest {
 
     HttpServletRequest request = mock(HttpServletRequest.class);
     when(request.getRemoteUser()).thenReturn(null);
+    
when(request.getAttribute(AuthConstants.AUTHENTICATED_PRINCIPAL_ATTRIBUTE_NAME))
+        .thenReturn(new UserPrincipal("admin"));
 
     ResourceConfig resourceConfig = new ResourceConfig();
     resourceConfig.register(IdpUserOperations.class);
diff --git a/scripts/h2/schema-2.0.0-h2.sql b/scripts/h2/schema-2.0.0-h2.sql
index 30c197e1bf..ba2a256b2d 100644
--- a/scripts/h2/schema-2.0.0-h2.sql
+++ b/scripts/h2/schema-2.0.0-h2.sql
@@ -255,6 +255,7 @@ CREATE TABLE IF NOT EXISTS `idp_user_meta` (
     `user_name` VARCHAR(128) NOT NULL COMMENT 'idp username',
     `password_hash` VARCHAR(1024) NOT NULL COMMENT 'idp user password hash',
     `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'whether the user is 
enabled, 0 is disabled, 1 is enabled',
+    `audit_info` CLOB NOT NULL COMMENT 'idp user audit info',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp user 
deleted at',
@@ -266,6 +267,7 @@ CREATE TABLE IF NOT EXISTS `idp_group_meta` (
     `group_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp group id',
     `group_name` VARCHAR(128) NOT NULL COMMENT 'idp group name',
     `group_comment` VARCHAR(1024) DEFAULT '' COMMENT 'idp group comment',
+    `audit_info` CLOB NOT NULL COMMENT 'idp group audit info',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp group 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp group last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp group 
deleted at',
@@ -277,6 +279,7 @@ CREATE TABLE IF NOT EXISTS `idp_user_group_rel` (
     `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment 
id',
     `user_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp user id',
     `group_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp group id',
+    `audit_info` CLOB NOT NULL COMMENT 'idp user group relation audit info',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp relation 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp relation last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp relation 
deleted at',
diff --git a/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql 
b/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
index 558ad6a79a..5d93944881 100644
--- a/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
+++ b/scripts/h2/upgrade-1.3.0-to-2.0.0-h2.sql
@@ -36,6 +36,12 @@ ALTER TABLE `idp_user_meta` ADD COLUMN `enabled` TINYINT(1) 
NOT NULL DEFAULT 1 C
 
 ALTER TABLE `idp_group_meta` ADD COLUMN `group_comment` VARCHAR(1024) DEFAULT 
'' COMMENT 'idp group comment' AFTER `group_name`;
 
+ALTER TABLE `idp_user_meta` ADD COLUMN `audit_info` CLOB NOT NULL DEFAULT '{}' 
COMMENT 'idp user audit info' AFTER `enabled`;
+
+ALTER TABLE `idp_group_meta` ADD COLUMN `audit_info` CLOB NOT NULL DEFAULT 
'{}' COMMENT 'idp group audit info' AFTER `group_comment`;
+
+ALTER TABLE `idp_user_group_rel` ADD COLUMN `audit_info` CLOB NOT NULL DEFAULT 
'{}' COMMENT 'idp user group relation audit info' AFTER `group_id`;
+
 CREATE UNIQUE INDEX IF NOT EXISTS `uk_ti_mi_mo_tv_del` ON `tag_relation_meta` 
(`tag_id`, `metadata_object_id`, `metadata_object_type`, `tag_value`, 
`deleted_at`);
 CREATE INDEX IF NOT EXISTS `idx_tid_value` ON `tag_relation_meta` (`tag_id`, 
`tag_value`);
 
diff --git a/scripts/mysql/schema-2.0.0-mysql.sql 
b/scripts/mysql/schema-2.0.0-mysql.sql
index 8f26d9f723..7831d811d8 100644
--- a/scripts/mysql/schema-2.0.0-mysql.sql
+++ b/scripts/mysql/schema-2.0.0-mysql.sql
@@ -246,6 +246,7 @@ CREATE TABLE IF NOT EXISTS `idp_user_meta` (
     `user_name` VARCHAR(128) NOT NULL COMMENT 'idp username',
     `password_hash` VARCHAR(1024) NOT NULL COMMENT 'idp user password hash',
     `enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'whether the user is 
enabled, 0 is disabled, 1 is enabled',
+    `audit_info` MEDIUMTEXT NOT NULL COMMENT 'idp user audit info',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp user last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp user 
deleted at',
@@ -257,6 +258,7 @@ CREATE TABLE IF NOT EXISTS `idp_group_meta` (
     `group_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp group id',
     `group_name` VARCHAR(128) NOT NULL COMMENT 'idp group name',
     `group_comment` VARCHAR(1024) DEFAULT '' COMMENT 'idp group comment',
+    `audit_info` MEDIUMTEXT NOT NULL COMMENT 'idp group audit info',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp group 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp group last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp group 
deleted at',
@@ -268,6 +270,7 @@ CREATE TABLE IF NOT EXISTS `idp_user_group_rel` (
     `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 'auto increment 
id',
     `user_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp user id',
     `group_id` BIGINT(20) UNSIGNED NOT NULL COMMENT 'idp group id',
+    `audit_info` MEDIUMTEXT NOT NULL COMMENT 'idp user group relation audit 
info',
     `current_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp relation 
current version',
     `last_version` INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 'idp relation last 
version',
     `deleted_at` BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT 'idp relation 
deleted at',
diff --git a/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql 
b/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
index f6fd86b02f..a603af129e 100644
--- a/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
+++ b/scripts/mysql/upgrade-1.3.0-to-2.0.0-mysql.sql
@@ -35,6 +35,37 @@ ALTER TABLE `idp_user_meta`
 ALTER TABLE `idp_group_meta`
     ADD COLUMN `group_comment` VARCHAR(1024) DEFAULT '' COMMENT 'idp group 
comment' AFTER `group_name`;
 
+-- add audit_info as nullable first for MySQL 5.7 compatibility (TEXT cannot 
have defaults)
+ALTER TABLE `idp_user_meta`
+    ADD COLUMN `audit_info` MEDIUMTEXT COMMENT 'idp user audit info' AFTER 
`enabled`;
+
+UPDATE `idp_user_meta`
+    SET `audit_info` = '{}'
+    WHERE `audit_info` IS NULL;
+
+ALTER TABLE `idp_user_meta`
+    MODIFY COLUMN `audit_info` MEDIUMTEXT NOT NULL COMMENT 'idp user audit 
info' AFTER `enabled`;
+
+ALTER TABLE `idp_group_meta`
+    ADD COLUMN `audit_info` MEDIUMTEXT COMMENT 'idp group audit info' AFTER 
`group_comment`;
+
+UPDATE `idp_group_meta`
+    SET `audit_info` = '{}'
+    WHERE `audit_info` IS NULL;
+
+ALTER TABLE `idp_group_meta`
+    MODIFY COLUMN `audit_info` MEDIUMTEXT NOT NULL COMMENT 'idp group audit 
info' AFTER `group_comment`;
+
+ALTER TABLE `idp_user_group_rel`
+    ADD COLUMN `audit_info` MEDIUMTEXT COMMENT 'idp user group relation audit 
info' AFTER `group_id`;
+
+UPDATE `idp_user_group_rel`
+    SET `audit_info` = '{}'
+    WHERE `audit_info` IS NULL;
+
+ALTER TABLE `idp_user_group_rel`
+    MODIFY COLUMN `audit_info` MEDIUMTEXT NOT NULL COMMENT 'idp user group 
relation audit info' AFTER `group_id`;
+
 CREATE UNIQUE INDEX `uk_ti_mi_mo_tv_del` ON `tag_relation_meta` (`tag_id`, 
`metadata_object_id`, `metadata_object_type`, `tag_value`, `deleted_at`);
 CREATE INDEX `idx_tid_value` ON `tag_relation_meta` (`tag_id`, `tag_value`);
 
diff --git a/scripts/postgresql/schema-2.0.0-postgresql.sql 
b/scripts/postgresql/schema-2.0.0-postgresql.sql
index 6e15ca5f40..b3a6812d15 100644
--- a/scripts/postgresql/schema-2.0.0-postgresql.sql
+++ b/scripts/postgresql/schema-2.0.0-postgresql.sql
@@ -434,6 +434,7 @@ CREATE TABLE IF NOT EXISTS idp_user_meta (
     user_name VARCHAR(128) NOT NULL,
     password_hash VARCHAR(1024) NOT NULL,
     enabled BOOLEAN NOT NULL DEFAULT TRUE,
+    audit_info TEXT NOT NULL,
     current_version INT NOT NULL DEFAULT 1,
     last_version INT NOT NULL DEFAULT 1,
     deleted_at BIGINT NOT NULL DEFAULT 0,
@@ -446,6 +447,7 @@ COMMENT ON COLUMN idp_user_meta.user_id IS 'idp user id';
 COMMENT ON COLUMN idp_user_meta.user_name IS 'idp username';
 COMMENT ON COLUMN idp_user_meta.password_hash IS 'idp user password hash';
 COMMENT ON COLUMN idp_user_meta.enabled IS 'whether the user is enabled, 0 is 
disabled, 1 is enabled';
+COMMENT ON COLUMN idp_user_meta.audit_info IS 'idp user audit info';
 COMMENT ON COLUMN idp_user_meta.current_version IS 'idp user current version';
 COMMENT ON COLUMN idp_user_meta.last_version IS 'idp user last version';
 COMMENT ON COLUMN idp_user_meta.deleted_at IS 'idp user deleted at';
@@ -454,6 +456,7 @@ CREATE TABLE IF NOT EXISTS idp_group_meta (
     group_id BIGINT NOT NULL,
     group_name VARCHAR(128) NOT NULL,
     group_comment VARCHAR(1024) DEFAULT '',
+    audit_info TEXT NOT NULL,
     current_version INT NOT NULL DEFAULT 1,
     last_version INT NOT NULL DEFAULT 1,
     deleted_at BIGINT NOT NULL DEFAULT 0,
@@ -465,6 +468,7 @@ COMMENT ON TABLE idp_group_meta IS 'local IdP group 
metadata';
 COMMENT ON COLUMN idp_group_meta.group_id IS 'idp group id';
 COMMENT ON COLUMN idp_group_meta.group_name IS 'idp group name';
 COMMENT ON COLUMN idp_group_meta.group_comment IS 'idp group comment';
+COMMENT ON COLUMN idp_group_meta.audit_info IS 'idp group audit info';
 COMMENT ON COLUMN idp_group_meta.current_version IS 'idp group current 
version';
 COMMENT ON COLUMN idp_group_meta.last_version IS 'idp group last version';
 COMMENT ON COLUMN idp_group_meta.deleted_at IS 'idp group deleted at';
@@ -473,6 +477,7 @@ CREATE TABLE IF NOT EXISTS idp_user_group_rel (
     id BIGINT NOT NULL GENERATED BY DEFAULT AS IDENTITY,
     user_id BIGINT NOT NULL,
     group_id BIGINT NOT NULL,
+    audit_info TEXT NOT NULL,
     current_version INT NOT NULL DEFAULT 1,
     last_version INT NOT NULL DEFAULT 1,
     deleted_at BIGINT NOT NULL DEFAULT 0,
@@ -486,6 +491,7 @@ COMMENT ON TABLE idp_user_group_rel IS 'local IdP user 
group relation';
 COMMENT ON COLUMN idp_user_group_rel.id IS 'auto increment id';
 COMMENT ON COLUMN idp_user_group_rel.user_id IS 'idp user id';
 COMMENT ON COLUMN idp_user_group_rel.group_id IS 'idp group id';
+COMMENT ON COLUMN idp_user_group_rel.audit_info IS 'idp user group relation 
audit info';
 COMMENT ON COLUMN idp_user_group_rel.current_version IS 'idp relation current 
version';
 COMMENT ON COLUMN idp_user_group_rel.last_version IS 'idp relation last 
version';
 COMMENT ON COLUMN idp_user_group_rel.deleted_at IS 'idp relation deleted at';
diff --git a/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql 
b/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
index 25940bec3b..7e780bb845 100644
--- a/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
+++ b/scripts/postgresql/upgrade-1.3.0-to-2.0.0-postgresql.sql
@@ -41,6 +41,15 @@ COMMENT ON COLUMN idp_user_meta.enabled IS 'whether the user 
is enabled, 0 is di
 ALTER TABLE idp_group_meta ADD COLUMN IF NOT EXISTS group_comment 
VARCHAR(1024) DEFAULT '';
 COMMENT ON COLUMN idp_group_meta.group_comment IS 'idp group comment';
 
+ALTER TABLE idp_user_meta ADD COLUMN IF NOT EXISTS audit_info TEXT NOT NULL 
DEFAULT '{}';
+COMMENT ON COLUMN idp_user_meta.audit_info IS 'idp user audit info';
+
+ALTER TABLE idp_group_meta ADD COLUMN IF NOT EXISTS audit_info TEXT NOT NULL 
DEFAULT '{}';
+COMMENT ON COLUMN idp_group_meta.audit_info IS 'idp group audit info';
+
+ALTER TABLE idp_user_group_rel ADD COLUMN IF NOT EXISTS audit_info TEXT NOT 
NULL DEFAULT '{}';
+COMMENT ON COLUMN idp_user_group_rel.audit_info IS 'idp user group relation 
audit info';
+
 ALTER TABLE tag_relation_meta DROP CONSTRAINT IF EXISTS 
tag_relation_meta_tag_id_metadata_object_id_metadata_object_key;
 
 CREATE UNIQUE INDEX IF NOT EXISTS uk_ti_mi_mo_tv_del ON tag_relation_meta 
(tag_id, metadata_object_id, metadata_object_type, tag_value, deleted_at);

Reply via email to