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 a9b2812549 [#14692] fix(idp-basic): Emit typed audit events for IdP 
mutations (#14693)
a9b2812549 is described below

commit a9b281254923d98000543fe800706a1e70db1af2
Author: MaSai <[email protected]>
AuthorDate: Fri Nov 6 17:57:29 2026 +0800

    [#14692] fix(idp-basic): Emit typed audit events for IdP mutations (#14693)
    
    ### What changes were proposed in this pull request?
    
    - Add `IdpUserGroupEventDispatcher` around `IdpUserGroupManager`,
    following the same PreEvent / Event / FailureEvent pattern as
    `AccessControlEventDispatcher` and `JobEventDispatcher`.
    - Add built-in IdP event types under
    `org.apache.gravitino.idp.listener.event`, with identifiers
    `basic.user.<name>` and `basic.group.<name>` (channel-prefixed,
    instance-scoped; parallel to forms such as `scim.user.<name>`).
    - Inject the dispatcher from `IdpBasicBinder` into IdP user/group REST
    resources so mutation success/failure events suppress the
    `HttpRequestEvent` / `HttpRequestFailureEvent` fallback.
    - Align null JSON body handling with metalake REST (`if (request ==
    null) { LOG.warn(...); return illegalArguments(...); }`), removing the
    previous nullable-Response helper.
    
    ### Why are the changes needed?
    
    Successful built-in IdP mutations (`/api/idp/users`, `/api/idp/groups`)
    did not dispatch operation-layer events. `HttpAuditFilter` therefore
    fell back to `HttpRequestEvent`, which always records `UNKNOWN` with a
    null object identifier. Auditors could see the HTTP method and URI, but
    not which user or group was created, enabled/disabled, password-changed,
    or membership-updated. Metalake user/group APIs already emit typed
    events through `AccessControlEventDispatcher`.
    
    Fixes #14692
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Audit log entries for built-in IdP mutations now use typed
    `OperationType` values (`ADD_USER`, `REMOVE_USER`, `ALTER_USER`,
    `ENABLE_USER`, `DISABLE_USER`, `ADD_GROUP`, `REMOVE_GROUP`,
    `ALTER_GROUP`) and name the object as `basic.user.<name>` /
    `basic.group.<name>` instead of `UNKNOWN` / `null`.
    
    Null request bodies on IdP write APIs still return HTTP 400 `Request
    body cannot be null` (behavior unchanged; only the handler style is
    aligned with server REST).
    
    ### How was this patch tested?
    
    - `./gradlew :plugins:idp-basic:test -PskipITs --tests
    org.apache.gravitino.idp.listener.TestIdpUserGroupEventDispatcher
    --tests org.apache.gravitino.idp.web.rest.TestIdpOperations`
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 docs/gravitino-server-config.md                    |   6 +-
 docs/security/local-users-and-groups.md            |  39 +++++
 .../idp/listener/IdpNameIdentifierUtil.java        |  56 +++++++
 .../idp/listener/IdpUserGroupEventDispatcher.java  | 185 +++++++++++++++++++++
 .../event/IdpAddGroupEvent.java}                   |  30 ++--
 .../event/IdpAddGroupFailureEvent.java}            |  30 ++--
 .../event/IdpAddGroupPreEvent.java}                |  30 ++--
 .../event/IdpAddUserEvent.java}                    |  30 ++--
 .../event/IdpAddUserFailureEvent.java}             |  30 ++--
 .../event/IdpAddUserPreEvent.java}                 |  30 ++--
 .../event/IdpChangeGroupMembershipEvent.java       |  69 ++++++++
 .../IdpChangeGroupMembershipFailureEvent.java      |  64 +++++++
 .../event/IdpChangeGroupMembershipPreEvent.java    |  60 +++++++
 .../event/IdpChangeUserPasswordEvent.java}         |  30 ++--
 .../event/IdpChangeUserPasswordFailureEvent.java}  |  30 ++--
 .../event/IdpChangeUserPasswordPreEvent.java}      |  30 ++--
 .../event/IdpGroupEvent.java}                      |  25 +--
 .../event/IdpGroupFailureEvent.java}               |  21 ++-
 .../event/IdpGroupPreEvent.java}                   |  21 ++-
 .../idp/listener/event/IdpRemoveGroupEvent.java    |  57 +++++++
 .../listener/event/IdpRemoveGroupFailureEvent.java |  51 ++++++
 .../event/IdpRemoveGroupPreEvent.java}             |  36 ++--
 .../idp/listener/event/IdpRemoveUserEvent.java     |  50 ++++++
 .../event/IdpRemoveUserFailureEvent.java}          |  30 ++--
 .../event/IdpRemoveUserPreEvent.java}              |  30 ++--
 .../listener/event/IdpUpdateUserEnabledEvent.java  |  50 ++++++
 .../event/IdpUpdateUserEnabledFailureEvent.java    |  51 ++++++
 .../event/IdpUpdateUserEnabledPreEvent.java        |  50 ++++++
 .../event/IdpUserEvent.java}                       |  25 +--
 .../event/IdpUserFailureEvent.java}                |  21 ++-
 .../event/IdpUserPreEvent.java}                    |  21 ++-
 .../org/apache/gravitino/idp/web/IdpRESTUtils.java |  17 --
 .../gravitino/idp/web/rest/IdpBasicBinder.java     |   7 +-
 .../gravitino/idp/web/rest/IdpGroupOperations.java |  36 ++--
 .../gravitino/idp/web/rest/IdpUserOperations.java  |  52 +++---
 .../listener/TestIdpUserGroupEventDispatcher.java  | 144 ++++++++++++++++
 .../gravitino/idp/web/rest/TestIdpOperations.java  |   6 +-
 37 files changed, 1309 insertions(+), 241 deletions(-)

diff --git a/docs/gravitino-server-config.md b/docs/gravitino-server-config.md
index 3c9d7647de..8d62c78662 100644
--- a/docs/gravitino-server-config.md
+++ b/docs/gravitino-server-config.md
@@ -615,9 +615,11 @@ Each operation emits up to three events: a pre-event 
before it runs, a post-even
 succeeds, and a failure event after it throws. The names follow the operation, 
so `createTable`
 produces `CreateTablePreEvent`, `CreateTableEvent`, and 
`CreateTableFailureEvent`. Operations
 served by the Gravitino IRC endpoint carry an `Iceberg` prefix, as in 
`IcebergCreateTableEvent`.
-Not every operation defines all three. The full set of classes lives in the
+Not every operation defines all three. Core metadata events live in the
 
[`org.apache.gravitino.listener.api.event`](https://github.com/apache/gravitino/tree/main/core/src/main/java/org/apache/gravitino/listener/api/event)
-package.
+package. Built-in IdP (`idp-basic`) user and group mutations additionally emit 
events from
+`org.apache.gravitino.idp.listener.event`, with object identifiers 
`basic.user.<name>` /
+`basic.group.<name>`; see [Local Users and 
Groups](./security/local-users-and-groups.md#audit-and-events).
 
 Throwing a `ForbiddenException` from a pre-event handler stops the operation 
before it runs, which
 makes pre-events a veto point rather than a notification.
diff --git a/docs/security/local-users-and-groups.md 
b/docs/security/local-users-and-groups.md
index 86048556a7..ad2e73d4df 100644
--- a/docs/security/local-users-and-groups.md
+++ b/docs/security/local-users-and-groups.md
@@ -151,6 +151,44 @@ curl -s -X PUT -H "Accept: 
application/vnd.gravitino.v1+json" \
   http://localhost:8090/api/idp/groups/engineering/users
 ```
 
+## Audit and Events
+
+Built-in IdP mutations go through `IdpUserGroupEventDispatcher`, the same 
PreEvent / Event /
+FailureEvent pattern used for metalake access-control and job operations. When
+[audit logging](../gravitino-server-config.md#audit-logging) or an
+[event listener](../gravitino-server-config.md#event-listeners) is enabled, 
successful and failed
+writes name the object and operation instead of falling back to 
`HttpRequestEvent` with
+`UNKNOWN` and a null identifier.
+
+Object identifiers are instance-scoped and channel-prefixed so they stay 
distinct from metalake
+users and groups:
+
+| Kind  | Identifier form      | Example            |
+|-------|----------------------|--------------------|
+| User  | `basic.user.<name>`  | `basic.user.alice` |
+| Group | `basic.group.<name>` | `basic.group.eng`  |
+
+The first segment is the authenticator / local-store channel (`basic`), 
parallel to other
+instance identity channels such as `scim.user.<name>`.
+
+| API                                  | Event family                | 
`OperationType`                |
+|--------------------------------------|-----------------------------|--------------------------------|
+| `POST /api/idp/users`                | `IdpAddUser*`               | 
`ADD_USER`                     |
+| `DELETE /api/idp/users/{user}`       | `IdpRemoveUser*`            | 
`REMOVE_USER`                  |
+| `PUT /api/idp/users/{user}/password` | `IdpChangeUserPassword*`    | 
`ALTER_USER`                   |
+| `PUT /api/idp/users/{user}/enabled`  | `IdpUpdateUserEnabled*`     | 
`ENABLE_USER` / `DISABLE_USER` |
+| `POST /api/idp/groups`               | `IdpAddGroup*`              | 
`ADD_GROUP`                    |
+| `DELETE /api/idp/groups/{group}`     | `IdpRemoveGroup*`           | 
`REMOVE_GROUP`                 |
+| `PUT /api/idp/groups/{group}/users`  | `IdpChangeGroupMembership*` | 
`ALTER_GROUP`                  |
+
+Each mutation emits a pre-event before the store write, a success event after 
it commits, or a
+failure event if it throws. Password values are not placed on the event. 
Membership changes carry
+`usersToAdd` / `usersToRemove` in `customInfo` (comma-separated). `GET` 
requests do not dispatch
+these operation events; with audit enabled they still produce the generic HTTP 
fallback entry.
+
+Event classes live in `org.apache.gravitino.idp.listener.event` inside the 
`idp-basic` plugin, not
+in the core `org.apache.gravitino.listener.api.event` package.
+
 ## Granting Access to Metadata
 
 A local user can authenticate as soon as it exists, but it can only reach 
metadata once it is registered in a metalake and granted privileges there. The 
two are separate steps, and the username must match.
@@ -227,5 +265,6 @@ iceberg.rest-catalog.http-headers=Authorization: Basic 
{base64_credentials}
 ## Further Reading
 
 - [OpenAPI definition](../open-api/idp/openapi.yaml) for full request and 
response schemas
+- [Audit Logging](../gravitino-server-config.md#audit-logging) and [Event 
Listeners](../gravitino-server-config.md#event-listeners) for observing IdP 
mutations
 - [How to Use HTTPS](how-to-use-https.md) for protecting credentials in transit
 - [Access Control](access-control.md) for what an authenticated user is 
allowed to do
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/IdpNameIdentifierUtil.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/IdpNameIdentifierUtil.java
new file mode 100644
index 0000000000..a1d9270a85
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/IdpNameIdentifierUtil.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.idp.listener;
+
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.NameIdentifier;
+
+/**
+ * Name identifiers for built-in IdP ({@code basic}) users and groups. These 
identities are
+ * instance-scoped (not metalake-scoped). The first namespace segment is the 
authenticator / local
+ * store channel ({@code basic}), parallel to other instance identity channels 
such as {@code
+ * scim.user.<name>}.
+ */
+public final class IdpNameIdentifierUtil {
+
+  /** Channel prefix for the built-in basic IdP local user store. */
+  public static final String BASIC_CHANNEL = "basic";
+
+  private IdpNameIdentifierUtil() {}
+
+  /**
+   * Creates a {@link NameIdentifier} for a built-in IdP user.
+   *
+   * @param userName the username
+   * @return {@code basic.user.<userName>}
+   */
+  public static NameIdentifier ofUser(String userName) {
+    return NameIdentifier.of(BASIC_CHANNEL, Entity.USER_SCHEMA_NAME, userName);
+  }
+
+  /**
+   * Creates a {@link NameIdentifier} for a built-in IdP group.
+   *
+   * @param groupName the group name
+   * @return {@code basic.group.<groupName>}
+   */
+  public static NameIdentifier ofGroup(String groupName) {
+    return NameIdentifier.of(BASIC_CHANNEL, Entity.GROUP_SCHEMA_NAME, 
groupName);
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/IdpUserGroupEventDispatcher.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/IdpUserGroupEventDispatcher.java
new file mode 100644
index 0000000000..508d4ed72a
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/IdpUserGroupEventDispatcher.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.idp.listener;
+
+import java.io.IOException;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.gravitino.idp.IdpUserGroupManager;
+import org.apache.gravitino.idp.listener.event.IdpAddGroupEvent;
+import org.apache.gravitino.idp.listener.event.IdpAddGroupFailureEvent;
+import org.apache.gravitino.idp.listener.event.IdpAddGroupPreEvent;
+import org.apache.gravitino.idp.listener.event.IdpAddUserEvent;
+import org.apache.gravitino.idp.listener.event.IdpAddUserFailureEvent;
+import org.apache.gravitino.idp.listener.event.IdpAddUserPreEvent;
+import org.apache.gravitino.idp.listener.event.IdpChangeGroupMembershipEvent;
+import 
org.apache.gravitino.idp.listener.event.IdpChangeGroupMembershipFailureEvent;
+import 
org.apache.gravitino.idp.listener.event.IdpChangeGroupMembershipPreEvent;
+import org.apache.gravitino.idp.listener.event.IdpChangeUserPasswordEvent;
+import 
org.apache.gravitino.idp.listener.event.IdpChangeUserPasswordFailureEvent;
+import org.apache.gravitino.idp.listener.event.IdpChangeUserPasswordPreEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveGroupEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveGroupFailureEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveGroupPreEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveUserEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveUserFailureEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveUserPreEvent;
+import org.apache.gravitino.idp.listener.event.IdpUpdateUserEnabledEvent;
+import 
org.apache.gravitino.idp.listener.event.IdpUpdateUserEnabledFailureEvent;
+import org.apache.gravitino.idp.listener.event.IdpUpdateUserEnabledPreEvent;
+import org.apache.gravitino.idp.model.IdpGroup;
+import org.apache.gravitino.idp.model.IdpUser;
+import org.apache.gravitino.listener.EventBus;
+import org.apache.gravitino.utils.PrincipalUtils;
+
+/**
+ * Dispatches pre/success/failure events around built-in IdP user and group 
mutations, matching the
+ * {@code *EventDispatcher} pattern used for metalake access-control and job 
operations.
+ *
+ * <p>Success and failure events mark the request thread so {@code 
HttpAuditFilter} does not emit a
+ * redundant {@code HttpRequestEvent} / {@code HttpRequestFailureEvent} with 
{@code UNKNOWN} and a
+ * null object identifier.
+ */
+public class IdpUserGroupEventDispatcher {
+
+  private final EventBus eventBus;
+  private final IdpUserGroupManager manager;
+
+  public IdpUserGroupEventDispatcher(EventBus eventBus, IdpUserGroupManager 
manager) {
+    this.eventBus = eventBus;
+    this.manager = manager;
+  }
+
+  public IdpUser getUser(String username) {
+    return manager.getUser(username);
+  }
+
+  public IdpGroup getGroup(String groupName) {
+    return manager.getGroup(groupName);
+  }
+
+  public IdpUser addUser(String username, String password, boolean enabled) 
throws IOException {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new IdpAddUserPreEvent(initiator, username));
+    try {
+      IdpUser user = manager.addUser(username, password, enabled);
+      eventBus.dispatchEvent(new IdpAddUserEvent(initiator, username));
+      return user;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new IdpAddUserFailureEvent(initiator, username, 
e));
+      throw e;
+    }
+  }
+
+  public boolean removeUser(String username) {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new IdpRemoveUserPreEvent(initiator, username));
+    try {
+      boolean isExists = manager.removeUser(username);
+      eventBus.dispatchEvent(new IdpRemoveUserEvent(initiator, username, 
isExists));
+      return isExists;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new IdpRemoveUserFailureEvent(initiator, 
username, e));
+      throw e;
+    }
+  }
+
+  public boolean changePassword(String username, String password) {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new IdpChangeUserPasswordPreEvent(initiator, 
username));
+    try {
+      boolean updated = manager.changePassword(username, password);
+      eventBus.dispatchEvent(new IdpChangeUserPasswordEvent(initiator, 
username));
+      return updated;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new IdpChangeUserPasswordFailureEvent(initiator, 
username, e));
+      throw e;
+    }
+  }
+
+  public boolean changePassword(String username, String newPassword, String 
oldPassword) {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new IdpChangeUserPasswordPreEvent(initiator, 
username));
+    try {
+      boolean updated = manager.changePassword(username, newPassword, 
oldPassword);
+      eventBus.dispatchEvent(new IdpChangeUserPasswordEvent(initiator, 
username));
+      return updated;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new IdpChangeUserPasswordFailureEvent(initiator, 
username, e));
+      throw e;
+    }
+  }
+
+  public boolean updateEnabled(String username, boolean enabled) {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new IdpUpdateUserEnabledPreEvent(initiator, 
username, enabled));
+    try {
+      boolean updated = manager.updateEnabled(username, enabled);
+      eventBus.dispatchEvent(new IdpUpdateUserEnabledEvent(initiator, 
username, enabled));
+      return updated;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new IdpUpdateUserEnabledFailureEvent(initiator, 
username, enabled, e));
+      throw e;
+    }
+  }
+
+  public IdpGroup addGroup(String groupName, String comment) throws 
IOException {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new IdpAddGroupPreEvent(initiator, groupName));
+    try {
+      IdpGroup group = manager.addGroup(groupName, comment);
+      eventBus.dispatchEvent(new IdpAddGroupEvent(initiator, groupName));
+      return group;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new IdpAddGroupFailureEvent(initiator, groupName, 
e));
+      throw e;
+    }
+  }
+
+  public boolean removeGroup(String groupName, boolean force) {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(new IdpRemoveGroupPreEvent(initiator, groupName, 
force));
+    try {
+      boolean isExists = manager.removeGroup(groupName, force);
+      eventBus.dispatchEvent(new IdpRemoveGroupEvent(initiator, groupName, 
isExists, force));
+      return isExists;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new IdpRemoveGroupFailureEvent(initiator, 
groupName, force, e));
+      throw e;
+    }
+  }
+
+  public IdpGroup changeGroupMembership(
+      String groupName, @Nullable List<String> usersToAdd, @Nullable 
List<String> usersToRemove) {
+    String initiator = PrincipalUtils.getCurrentUserName();
+    eventBus.dispatchEvent(
+        new IdpChangeGroupMembershipPreEvent(initiator, groupName, usersToAdd, 
usersToRemove));
+    try {
+      IdpGroup group = manager.changeGroupMembership(groupName, usersToAdd, 
usersToRemove);
+      eventBus.dispatchEvent(
+          new IdpChangeGroupMembershipEvent(initiator, groupName, usersToAdd, 
usersToRemove));
+      return group;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(
+          new IdpChangeGroupMembershipFailureEvent(
+              initiator, groupName, usersToAdd, usersToRemove, e));
+      throw e;
+    }
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupEvent.java
similarity index 54%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupEvent.java
index ef34e210ce..c7ebebf14c 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Success event for a built-in IdP IdpAddGroup operation. */
+@DeveloperApi
+public class IdpAddGroupEvent extends IdpGroupEvent {
+  private final String groupName;
+
+  public IdpAddGroupEvent(String initiator, String groupName) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName));
+    this.groupName = groupName;
+  }
+
+  public String groupName() {
+    return groupName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ADD_GROUP;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupFailureEvent.java
similarity index 52%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupFailureEvent.java
index ef34e210ce..76333d1abb 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupFailureEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Failure event for a built-in IdP IdpAddGroup operation. */
+@DeveloperApi
+public class IdpAddGroupFailureEvent extends IdpGroupFailureEvent {
+  private final String groupName;
+
+  public IdpAddGroupFailureEvent(String initiator, String groupName, Exception 
exception) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName), exception);
+    this.groupName = groupName;
+  }
+
+  public String groupName() {
+    return groupName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ADD_GROUP;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupPreEvent.java
similarity index 54%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupPreEvent.java
index ef34e210ce..8c89eb8e16 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddGroupPreEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Pre-event for a built-in IdP IdpAddGroup operation. */
+@DeveloperApi
+public class IdpAddGroupPreEvent extends IdpGroupPreEvent {
+  private final String groupName;
+
+  public IdpAddGroupPreEvent(String initiator, String groupName) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName));
+    this.groupName = groupName;
+  }
+
+  public String groupName() {
+    return groupName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ADD_GROUP;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserEvent.java
similarity index 55%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserEvent.java
index ef34e210ce..5d8567017c 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Success event for adding a built-in IdP user. */
+@DeveloperApi
+public class IdpAddUserEvent extends IdpUserEvent {
+  private final String userName;
+
+  public IdpAddUserEvent(String initiator, String userName) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName));
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ADD_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserFailureEvent.java
similarity index 53%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserFailureEvent.java
index ef34e210ce..4d4ac79595 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserFailureEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Failure event for adding a built-in IdP user. */
+@DeveloperApi
+public class IdpAddUserFailureEvent extends IdpUserFailureEvent {
+  private final String userName;
+
+  public IdpAddUserFailureEvent(String initiator, String userName, Exception 
exception) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName), exception);
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ADD_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserPreEvent.java
similarity index 55%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserPreEvent.java
index ef34e210ce..f37f6d6806 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpAddUserPreEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Pre-event for adding a built-in IdP user. */
+@DeveloperApi
+public class IdpAddUserPreEvent extends IdpUserPreEvent {
+  private final String userName;
+
+  public IdpAddUserPreEvent(String initiator, String userName) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName));
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ADD_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipEvent.java
new file mode 100644
index 0000000000..6b107c0d66
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipEvent.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Success event for changing built-in IdP group membership. */
+@DeveloperApi
+public class IdpChangeGroupMembershipEvent extends IdpGroupEvent {
+  private final String groupName;
+  private final List<String> usersToAdd;
+  private final List<String> usersToRemove;
+
+  public IdpChangeGroupMembershipEvent(
+      String initiator, String groupName, List<String> usersToAdd, 
List<String> usersToRemove) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName));
+    this.groupName = groupName;
+    this.usersToAdd = usersToAdd == null ? Collections.emptyList() : 
List.copyOf(usersToAdd);
+    this.usersToRemove =
+        usersToRemove == null ? Collections.emptyList() : 
List.copyOf(usersToRemove);
+  }
+
+  public String groupName() {
+    return groupName;
+  }
+
+  public List<String> usersToAdd() {
+    return usersToAdd;
+  }
+
+  public List<String> usersToRemove() {
+    return usersToRemove;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.ALTER_GROUP;
+  }
+
+  @Override
+  protected Map<String, String> ownCustomInfo() {
+    return ImmutableMap.of(
+        "usersToAdd", String.join(",", usersToAdd),
+        "usersToRemove", String.join(",", usersToRemove));
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipFailureEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipFailureEvent.java
new file mode 100644
index 0000000000..354345467c
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipFailureEvent.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Failure event for changing built-in IdP group membership. */
+@DeveloperApi
+public class IdpChangeGroupMembershipFailureEvent extends IdpGroupFailureEvent 
{
+  private final String groupName;
+  private final List<String> usersToAdd;
+  private final List<String> usersToRemove;
+
+  public IdpChangeGroupMembershipFailureEvent(
+      String initiator,
+      String groupName,
+      List<String> usersToAdd,
+      List<String> usersToRemove,
+      Exception exception) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName), exception);
+    this.groupName = groupName;
+    this.usersToAdd = usersToAdd == null ? Collections.emptyList() : 
List.copyOf(usersToAdd);
+    this.usersToRemove =
+        usersToRemove == null ? Collections.emptyList() : 
List.copyOf(usersToRemove);
+  }
+
+  public String groupName() {
+    return groupName;
+  }
+
+  public List<String> usersToAdd() {
+    return usersToAdd;
+  }
+
+  public List<String> usersToRemove() {
+    return usersToRemove;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.ALTER_GROUP;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipPreEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipPreEvent.java
new file mode 100644
index 0000000000..91a0fa025d
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeGroupMembershipPreEvent.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import java.util.Collections;
+import java.util.List;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Pre-event for changing built-in IdP group membership. */
+@DeveloperApi
+public class IdpChangeGroupMembershipPreEvent extends IdpGroupPreEvent {
+  private final String groupName;
+  private final List<String> usersToAdd;
+  private final List<String> usersToRemove;
+
+  public IdpChangeGroupMembershipPreEvent(
+      String initiator, String groupName, List<String> usersToAdd, 
List<String> usersToRemove) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName));
+    this.groupName = groupName;
+    this.usersToAdd = usersToAdd == null ? Collections.emptyList() : 
List.copyOf(usersToAdd);
+    this.usersToRemove =
+        usersToRemove == null ? Collections.emptyList() : 
List.copyOf(usersToRemove);
+  }
+
+  public String groupName() {
+    return groupName;
+  }
+
+  public List<String> usersToAdd() {
+    return usersToAdd;
+  }
+
+  public List<String> usersToRemove() {
+    return usersToRemove;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.ALTER_GROUP;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordEvent.java
similarity index 53%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordEvent.java
index ef34e210ce..a8e9074c24 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Success event for changing a built-in IdP user password. */
+@DeveloperApi
+public class IdpChangeUserPasswordEvent extends IdpUserEvent {
+  private final String userName;
+
+  public IdpChangeUserPasswordEvent(String initiator, String userName) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName));
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ALTER_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordFailureEvent.java
similarity index 52%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordFailureEvent.java
index ef34e210ce..c2f50be9d7 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordFailureEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Failure event for changing a built-in IdP user password. */
+@DeveloperApi
+public class IdpChangeUserPasswordFailureEvent extends IdpUserFailureEvent {
+  private final String userName;
+
+  public IdpChangeUserPasswordFailureEvent(String initiator, String userName, 
Exception exception) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName), exception);
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ALTER_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordPreEvent.java
similarity index 53%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordPreEvent.java
index ef34e210ce..1322ea0e9a 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpChangeUserPasswordPreEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Pre-event for changing a built-in IdP user password. */
+@DeveloperApi
+public class IdpChangeUserPasswordPreEvent extends IdpUserPreEvent {
+  private final String userName;
+
+  public IdpChangeUserPasswordPreEvent(String initiator, String userName) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName));
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.ALTER_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupEvent.java
similarity index 58%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupEvent.java
index ef34e210ce..9113683a25 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupEvent.java
@@ -16,19 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.Event;
+import org.apache.gravitino.listener.api.event.OperationStatus;
+
+/** Base success event for built-in IdP group mutations. */
+@DeveloperApi
+public abstract class IdpGroupEvent extends Event {
+
+  protected IdpGroupEvent(String initiator, NameIdentifier identifier) {
+    super(initiator, identifier);
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationStatus operationStatus() {
+    return OperationStatus.SUCCESS;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupFailureEvent.java
similarity index 59%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupFailureEvent.java
index ef34e210ce..178482a6c0 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupFailureEvent.java
@@ -16,19 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.FailureEvent;
 
-  @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+/** Base failure event for built-in IdP group mutations. */
+@DeveloperApi
+public abstract class IdpGroupFailureEvent extends FailureEvent {
+
+  protected IdpGroupFailureEvent(String initiator, NameIdentifier identifier, 
Exception exception) {
+    super(initiator, identifier, exception);
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupPreEvent.java
similarity index 59%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupPreEvent.java
index ef34e210ce..5c1e292adb 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpGroupPreEvent.java
@@ -16,19 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.PreEvent;
 
-  @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+/** Base pre-event for built-in IdP group mutations. */
+@DeveloperApi
+public abstract class IdpGroupPreEvent extends PreEvent {
+
+  protected IdpGroupPreEvent(String initiator, NameIdentifier identifier) {
+    super(initiator, identifier);
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupEvent.java
new file mode 100644
index 0000000000..4489317e6e
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Success event for removing a built-in IdP group. */
+@DeveloperApi
+public class IdpRemoveGroupEvent extends IdpGroupEvent {
+  private final String removedGroupName;
+  private final boolean isExists;
+  private final boolean force;
+
+  public IdpRemoveGroupEvent(
+      String initiator, String removedGroupName, boolean isExists, boolean 
force) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(removedGroupName));
+    this.removedGroupName = removedGroupName;
+    this.isExists = isExists;
+    this.force = force;
+  }
+
+  public String removedGroupName() {
+    return removedGroupName;
+  }
+
+  public boolean isExists() {
+    return isExists;
+  }
+
+  public boolean force() {
+    return force;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.REMOVE_GROUP;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupFailureEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupFailureEvent.java
new file mode 100644
index 0000000000..94c2edafa9
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupFailureEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Failure event for removing a built-in IdP group. */
+@DeveloperApi
+public class IdpRemoveGroupFailureEvent extends IdpGroupFailureEvent {
+  private final String groupName;
+  private final boolean force;
+
+  public IdpRemoveGroupFailureEvent(
+      String initiator, String groupName, boolean force, Exception exception) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName), exception);
+    this.groupName = groupName;
+    this.force = force;
+  }
+
+  public String groupName() {
+    return groupName;
+  }
+
+  public boolean force() {
+    return force;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.REMOVE_GROUP;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupPreEvent.java
similarity index 50%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupPreEvent.java
index ef34e210ce..5976a75a8b 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveGroupPreEvent.java
@@ -16,19 +16,35 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Pre-event for removing a built-in IdP group. */
+@DeveloperApi
+public class IdpRemoveGroupPreEvent extends IdpGroupPreEvent {
+  private final String groupName;
+  private final boolean force;
+
+  public IdpRemoveGroupPreEvent(String initiator, String groupName, boolean 
force) {
+    super(initiator, IdpNameIdentifierUtil.ofGroup(groupName));
+    this.groupName = groupName;
+    this.force = force;
+  }
+
+  public String groupName() {
+    return groupName;
+  }
+
+  public boolean force() {
+    return force;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.REMOVE_GROUP;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserEvent.java
new file mode 100644
index 0000000000..9452f9edbf
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Success event for removing a built-in IdP user. */
+@DeveloperApi
+public class IdpRemoveUserEvent extends IdpUserEvent {
+  private final String removedUserName;
+  private final boolean isExists;
+
+  public IdpRemoveUserEvent(String initiator, String removedUserName, boolean 
isExists) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(removedUserName));
+    this.removedUserName = removedUserName;
+    this.isExists = isExists;
+  }
+
+  public String removedUserName() {
+    return removedUserName;
+  }
+
+  public boolean isExists() {
+    return isExists;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.REMOVE_USER;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserFailureEvent.java
similarity index 52%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserFailureEvent.java
index ef34e210ce..02faf6fc54 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserFailureEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Failure event for removing a built-in IdP user. */
+@DeveloperApi
+public class IdpRemoveUserFailureEvent extends IdpUserFailureEvent {
+  private final String userName;
+
+  public IdpRemoveUserFailureEvent(String initiator, String userName, 
Exception exception) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName), exception);
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.REMOVE_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserPreEvent.java
similarity index 54%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserPreEvent.java
index ef34e210ce..98f058d264 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpRemoveUserPreEvent.java
@@ -16,19 +16,29 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Pre-event for removing a built-in IdP user. */
+@DeveloperApi
+public class IdpRemoveUserPreEvent extends IdpUserPreEvent {
+  private final String userName;
+
+  public IdpRemoveUserPreEvent(String initiator, String userName) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName));
+    this.userName = userName;
+  }
+
+  public String userName() {
+    return userName;
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationType operationType() {
+    return OperationType.REMOVE_USER;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledEvent.java
new file mode 100644
index 0000000000..ff299aa4bf
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** Event for enabling or disabling a built-in IdP user. */
+@DeveloperApi
+public class IdpUpdateUserEnabledEvent extends IdpUserEvent {
+  private final String userName;
+  private final boolean enabled;
+
+  public IdpUpdateUserEnabledEvent(String initiator, String userName, boolean 
enabled) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName));
+    this.userName = userName;
+    this.enabled = enabled;
+  }
+
+  public String userName() {
+    return userName;
+  }
+
+  public boolean enabled() {
+    return enabled;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return enabled ? OperationType.ENABLE_USER : OperationType.DISABLE_USER;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledFailureEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledFailureEvent.java
new file mode 100644
index 0000000000..5ebe408ac8
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledFailureEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** FailureEvent for enabling or disabling a built-in IdP user. */
+@DeveloperApi
+public class IdpUpdateUserEnabledFailureEvent extends IdpUserFailureEvent {
+  private final String userName;
+  private final boolean enabled;
+
+  public IdpUpdateUserEnabledFailureEvent(
+      String initiator, String userName, boolean enabled, Exception exception) 
{
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName), exception);
+    this.userName = userName;
+    this.enabled = enabled;
+  }
+
+  public String userName() {
+    return userName;
+  }
+
+  public boolean enabled() {
+    return enabled;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return enabled ? OperationType.ENABLE_USER : OperationType.DISABLE_USER;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledPreEvent.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledPreEvent.java
new file mode 100644
index 0000000000..a9c34661d0
--- /dev/null
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUpdateUserEnabledPreEvent.java
@@ -0,0 +1,50 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.idp.listener.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.idp.listener.IdpNameIdentifierUtil;
+import org.apache.gravitino.listener.api.event.OperationType;
+
+/** PreEvent for enabling or disabling a built-in IdP user. */
+@DeveloperApi
+public class IdpUpdateUserEnabledPreEvent extends IdpUserPreEvent {
+  private final String userName;
+  private final boolean enabled;
+
+  public IdpUpdateUserEnabledPreEvent(String initiator, String userName, 
boolean enabled) {
+    super(initiator, IdpNameIdentifierUtil.ofUser(userName));
+    this.userName = userName;
+    this.enabled = enabled;
+  }
+
+  public String userName() {
+    return userName;
+  }
+
+  public boolean enabled() {
+    return enabled;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return enabled ? OperationType.ENABLE_USER : OperationType.DISABLE_USER;
+  }
+}
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserEvent.java
similarity index 58%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserEvent.java
index ef34e210ce..404443ecbd 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserEvent.java
@@ -16,19 +16,24 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.Event;
+import org.apache.gravitino.listener.api.event.OperationStatus;
+
+/** Base success event for built-in IdP user mutations. */
+@DeveloperApi
+public abstract class IdpUserEvent extends Event {
+
+  protected IdpUserEvent(String initiator, NameIdentifier identifier) {
+    super(initiator, identifier);
+  }
 
   @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+  public OperationStatus operationStatus() {
+    return OperationStatus.SUCCESS;
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserFailureEvent.java
similarity index 59%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserFailureEvent.java
index ef34e210ce..ea522c7132 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserFailureEvent.java
@@ -16,19 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.FailureEvent;
 
-  @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+/** Base failure event for built-in IdP user mutations. */
+@DeveloperApi
+public abstract class IdpUserFailureEvent extends FailureEvent {
+
+  protected IdpUserFailureEvent(String initiator, NameIdentifier identifier, 
Exception exception) {
+    super(initiator, identifier, exception);
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserPreEvent.java
similarity index 59%
copy from 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
copy to 
plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserPreEvent.java
index ef34e210ce..a2212c31b7 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/listener/event/IdpUserPreEvent.java
@@ -16,19 +16,18 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-package org.apache.gravitino.idp.web.rest;
 
-import org.apache.gravitino.GravitinoEnv;
-import org.apache.gravitino.idp.IdpUserGroupManager;
-import org.glassfish.hk2.utilities.binding.AbstractBinder;
+package org.apache.gravitino.idp.listener.event;
 
-/** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
-public class IdpBasicBinder extends AbstractBinder {
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.listener.api.event.PreEvent;
 
-  @Override
-  protected void configure() {
-    GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+/** Base pre-event for built-in IdP user mutations. */
+@DeveloperApi
+public abstract class IdpUserPreEvent extends PreEvent {
+
+  protected IdpUserPreEvent(String initiator, NameIdentifier identifier) {
+    super(initiator, identifier);
   }
 }
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 a0672f8492..324bbf2f46 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/IdpRESTUtils.java
@@ -19,7 +19,6 @@
 package org.apache.gravitino.idp.web;
 
 import java.security.PrivilegedExceptionAction;
-import javax.annotation.Nullable;
 import javax.servlet.http.HttpServletRequest;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
@@ -83,22 +82,6 @@ public final class IdpRESTUtils {
     return toErrorResponse(errorMsg, e);
   }
 
-  /**
-   * Returns a 400 response when {@code request} is null; otherwise {@code 
null}.
-   *
-   * @param request the deserialized request body, which may be null
-   * @return a bad-request response if {@code request} is null, otherwise 
{@code null}
-   */
-  @Nullable
-  public static Response rejectNullRequestBody(Object request) {
-    if (request != null) {
-      return null;
-    }
-    LOG.warn("Received IdP request with null request body");
-    return illegalArguments(
-        "Request body cannot be null", new IllegalArgumentException("Request 
body cannot be null"));
-  }
-
   public static Response illegalArguments(String message, Throwable throwable) 
{
     return json(
         Response.Status.BAD_REQUEST,
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
index ef34e210ce..9dc519df6d 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java
@@ -20,6 +20,7 @@ package org.apache.gravitino.idp.web.rest;
 
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.idp.IdpUserGroupManager;
+import org.apache.gravitino.idp.listener.IdpUserGroupEventDispatcher;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 
 /** Registers built-in IdP REST dependencies for Jersey/HK2 injection. */
@@ -28,7 +29,9 @@ public class IdpBasicBinder extends AbstractBinder {
   @Override
   protected void configure() {
     GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance();
-    bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator()))
-        .to(IdpUserGroupManager.class);
+    IdpUserGroupManager manager =
+        IdpUserGroupManager.getInstance(gravitinoEnv.config(), 
gravitinoEnv.idGenerator());
+    bind(new IdpUserGroupEventDispatcher(gravitinoEnv.eventBus(), manager))
+        .to(IdpUserGroupEventDispatcher.class);
   }
 }
diff --git 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
index 7d94cffea1..4f48e2a1ee 100644
--- 
a/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
+++ 
b/plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpGroupOperations.java
@@ -35,27 +35,31 @@ import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.Response;
 import org.apache.gravitino.dto.responses.RemoveResponse;
-import org.apache.gravitino.idp.IdpUserGroupManager;
 import org.apache.gravitino.idp.dto.requests.AddGroupRequest;
 import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest;
 import org.apache.gravitino.idp.dto.responses.IdpGroupResponse;
+import org.apache.gravitino.idp.listener.IdpUserGroupEventDispatcher;
 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.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** REST resource for built-in IdP group management exposed by the {@code 
idp-basic} plugin. */
 @IdpManagement
 @Path("/idp/groups")
 public class IdpGroupOperations {
 
-  private final IdpUserGroupManager userGroupManager;
+  private static final Logger LOG = 
LoggerFactory.getLogger(IdpGroupOperations.class);
+
+  private final IdpUserGroupEventDispatcher userGroupDispatcher;
 
   @Context private HttpServletRequest httpRequest;
 
   @Inject
-  public IdpGroupOperations(IdpUserGroupManager userGroupManager) {
-    this.userGroupManager = userGroupManager;
+  public IdpGroupOperations(IdpUserGroupEventDispatcher userGroupDispatcher) {
+    this.userGroupDispatcher = userGroupDispatcher;
   }
 
   @GET
@@ -66,7 +70,7 @@ public class IdpGroupOperations {
   public Response getGroup(@PathParam("group") String group) {
     return IdpRESTUtils.doAs(
         httpRequest,
-        () -> IdpRESTUtils.ok(new 
IdpGroupResponse(userGroupManager.getGroup(group).toDTO())),
+        () -> IdpRESTUtils.ok(new 
IdpGroupResponse(userGroupDispatcher.getGroup(group).toDTO())),
         "group",
         IdpOperationType.GET,
         group);
@@ -77,9 +81,11 @@ public class IdpGroupOperations {
   @Timed(name = "add-idp-group." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "add-idp-group", absolute = true)
   public Response addGroup(AddGroupRequest request) {
-    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
-    if (rejection != null) {
-      return rejection;
+    if (request == null) {
+      LOG.warn("Received IdP request with null request body");
+      return IdpRESTUtils.illegalArguments(
+          "Request body cannot be null",
+          new IllegalArgumentException("Request body cannot be null"));
     }
     return IdpRESTUtils.doAs(
         httpRequest,
@@ -87,7 +93,7 @@ public class IdpGroupOperations {
           request.validate();
           return IdpRESTUtils.ok(
               new IdpGroupResponse(
-                  userGroupManager.addGroup(request.getGroup(), 
request.getComment()).toDTO()));
+                  userGroupDispatcher.addGroup(request.getGroup(), 
request.getComment()).toDTO()));
         },
         "group",
         IdpOperationType.ADD,
@@ -103,7 +109,7 @@ public class IdpGroupOperations {
       @PathParam("group") String group, @DefaultValue("false") 
@QueryParam("force") boolean force) {
     return IdpRESTUtils.doAs(
         httpRequest,
-        () -> IdpRESTUtils.ok(new 
RemoveResponse(userGroupManager.removeGroup(group, force))),
+        () -> IdpRESTUtils.ok(new 
RemoveResponse(userGroupDispatcher.removeGroup(group, force))),
         "group",
         IdpOperationType.REMOVE,
         group);
@@ -116,9 +122,11 @@ public class IdpGroupOperations {
   @ResponseMetered(name = "change-idp-group-users", absolute = true)
   public Response changeGroupMembership(
       @PathParam("group") String group, GroupMembershipChangeRequest request) {
-    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
-    if (rejection != null) {
-      return rejection;
+    if (request == null) {
+      LOG.warn("Received IdP request with null request body");
+      return IdpRESTUtils.illegalArguments(
+          "Request body cannot be null",
+          new IllegalArgumentException("Request body cannot be null"));
     }
     return IdpRESTUtils.doAs(
         httpRequest,
@@ -128,7 +136,7 @@ public class IdpGroupOperations {
           String[] usersToRemove = request.getUsersToRemove();
           return IdpRESTUtils.ok(
               new IdpGroupResponse(
-                  userGroupManager
+                  userGroupDispatcher
                       .changeGroupMembership(
                           group,
                           usersToAdd == null ? null : 
Arrays.asList(usersToAdd),
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 11fd3d84ef..2582fddd56 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
@@ -38,29 +38,33 @@ 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.ChangePasswordRequest;
 import org.apache.gravitino.idp.dto.requests.UpdateUserEnabledRequest;
 import org.apache.gravitino.idp.dto.responses.IdpUserResponse;
+import org.apache.gravitino.idp.listener.IdpUserGroupEventDispatcher;
 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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** REST resource for built-in IdP user management exposed by the {@code 
idp-basic} plugin. */
 @IdpManagement
 @Path("/idp/users")
 public class IdpUserOperations {
 
-  private final IdpUserGroupManager userGroupManager;
+  private static final Logger LOG = 
LoggerFactory.getLogger(IdpUserOperations.class);
+
+  private final IdpUserGroupEventDispatcher userGroupDispatcher;
 
   @Context private HttpServletRequest httpRequest;
 
   @Inject
-  public IdpUserOperations(IdpUserGroupManager userGroupManager) {
-    this.userGroupManager = userGroupManager;
+  public IdpUserOperations(IdpUserGroupEventDispatcher userGroupDispatcher) {
+    this.userGroupDispatcher = userGroupDispatcher;
   }
 
   @GET
@@ -71,7 +75,7 @@ public class IdpUserOperations {
   public Response getUser(@PathParam("user") String user) {
     return IdpRESTUtils.doAs(
         httpRequest,
-        () -> IdpRESTUtils.ok(new 
IdpUserResponse(userGroupManager.getUser(user).toDTO())),
+        () -> IdpRESTUtils.ok(new 
IdpUserResponse(userGroupDispatcher.getUser(user).toDTO())),
         "user",
         IdpOperationType.GET,
         user);
@@ -82,9 +86,11 @@ public class IdpUserOperations {
   @Timed(name = "add-idp-user." + MetricNames.HTTP_PROCESS_DURATION, absolute 
= true)
   @ResponseMetered(name = "add-idp-user", absolute = true)
   public Response addUser(AddUserRequest request) {
-    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
-    if (rejection != null) {
-      return rejection;
+    if (request == null) {
+      LOG.warn("Received IdP request with null request body");
+      return IdpRESTUtils.illegalArguments(
+          "Request body cannot be null",
+          new IllegalArgumentException("Request body cannot be null"));
     }
     return IdpRESTUtils.doAs(
         httpRequest,
@@ -92,7 +98,7 @@ public class IdpUserOperations {
           request.validate();
           return IdpRESTUtils.ok(
               new IdpUserResponse(
-                  userGroupManager
+                  userGroupDispatcher
                       .addUser(request.getUser(), request.getPassword(), 
request.enabledOrDefault())
                       .toDTO()));
         },
@@ -115,9 +121,11 @@ public class IdpUserOperations {
   @Timed(name = "change-idp-user-password." + 
MetricNames.HTTP_PROCESS_DURATION, absolute = true)
   @ResponseMetered(name = "change-idp-user-password", absolute = true)
   public Response changePassword(@PathParam("user") String user, 
ChangePasswordRequest request) {
-    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
-    if (rejection != null) {
-      return rejection;
+    if (request == null) {
+      LOG.warn("Received IdP request with null request body");
+      return IdpRESTUtils.illegalArguments(
+          "Request body cannot be null",
+          new IllegalArgumentException("Request body cannot be null"));
     }
     return IdpRESTUtils.doAs(
         httpRequest,
@@ -135,15 +143,15 @@ public class IdpUserOperations {
             Preconditions.checkArgument(
                 StringUtils.isNotBlank(request.getOldPassword()),
                 "\"oldPassword\" is required when changing your own password");
-            userGroupManager.changePassword(
+            userGroupDispatcher.changePassword(
                 user, request.getNewPassword(), request.getOldPassword());
           } else {
             Preconditions.checkArgument(
                 request.getOldPassword() == null,
                 "\"oldPassword\" is only allowed when changing your own 
password");
-            userGroupManager.changePassword(user, request.getNewPassword());
+            userGroupDispatcher.changePassword(user, request.getNewPassword());
           }
-          return IdpRESTUtils.ok(new 
IdpUserResponse(userGroupManager.getUser(user).toDTO()));
+          return IdpRESTUtils.ok(new 
IdpUserResponse(userGroupDispatcher.getUser(user).toDTO()));
         },
         "user",
         IdpOperationType.UPDATE,
@@ -164,9 +172,11 @@ public class IdpUserOperations {
   @Timed(name = "update-idp-user-enabled." + 
MetricNames.HTTP_PROCESS_DURATION, absolute = true)
   @ResponseMetered(name = "update-idp-user-enabled", absolute = true)
   public Response updateEnabled(@PathParam("user") String user, 
UpdateUserEnabledRequest request) {
-    Response rejection = IdpRESTUtils.rejectNullRequestBody(request);
-    if (rejection != null) {
-      return rejection;
+    if (request == null) {
+      LOG.warn("Received IdP request with null request body");
+      return IdpRESTUtils.illegalArguments(
+          "Request body cannot be null",
+          new IllegalArgumentException("Request body cannot be null"));
     }
     return IdpRESTUtils.doAs(
         httpRequest,
@@ -182,8 +192,8 @@ public class IdpUserOperations {
               !IdpAuthorizationFilter.isServiceAdmin(serviceAdmins, user),
               "Cannot update the enabled flag of service admin %s",
               user);
-          userGroupManager.updateEnabled(user, request.getEnabled());
-          return IdpRESTUtils.ok(new 
IdpUserResponse(userGroupManager.getUser(user).toDTO()));
+          userGroupDispatcher.updateEnabled(user, request.getEnabled());
+          return IdpRESTUtils.ok(new 
IdpUserResponse(userGroupDispatcher.getUser(user).toDTO()));
         },
         "user",
         IdpOperationType.UPDATE,
@@ -198,7 +208,7 @@ public class IdpUserOperations {
   public Response removeUser(@PathParam("user") String user) {
     return IdpRESTUtils.doAs(
         httpRequest,
-        () -> IdpRESTUtils.ok(new 
RemoveResponse(userGroupManager.removeUser(user))),
+        () -> IdpRESTUtils.ok(new 
RemoveResponse(userGroupDispatcher.removeUser(user))),
         "user",
         IdpOperationType.REMOVE,
         user);
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/listener/TestIdpUserGroupEventDispatcher.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/listener/TestIdpUserGroupEventDispatcher.java
new file mode 100644
index 0000000000..cc05e194c6
--- /dev/null
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/listener/TestIdpUserGroupEventDispatcher.java
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.idp.listener;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.security.PrivilegedExceptionAction;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.exceptions.AlreadyExistsException;
+import org.apache.gravitino.idp.IdpUserGroupManager;
+import org.apache.gravitino.idp.listener.event.IdpAddGroupEvent;
+import org.apache.gravitino.idp.listener.event.IdpAddUserEvent;
+import org.apache.gravitino.idp.listener.event.IdpAddUserFailureEvent;
+import org.apache.gravitino.idp.listener.event.IdpChangeGroupMembershipEvent;
+import org.apache.gravitino.idp.listener.event.IdpChangeUserPasswordEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveGroupEvent;
+import org.apache.gravitino.idp.listener.event.IdpRemoveUserEvent;
+import org.apache.gravitino.idp.listener.event.IdpUpdateUserEnabledEvent;
+import org.apache.gravitino.idp.model.IdpGroup;
+import org.apache.gravitino.idp.model.IdpUser;
+import org.apache.gravitino.listener.EventBus;
+import org.apache.gravitino.listener.api.EventListenerPlugin;
+import org.apache.gravitino.listener.api.event.Event;
+import org.apache.gravitino.listener.api.event.OperationType;
+import org.apache.gravitino.listener.api.event.PreEvent;
+import org.apache.gravitino.utils.PrincipalUtils;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestIdpUserGroupEventDispatcher {
+
+  private final List<Event> postEvents = new ArrayList<>();
+  private IdpUserGroupManager manager;
+  private IdpUserGroupEventDispatcher dispatcher;
+
+  @BeforeEach
+  void setUp() {
+    postEvents.clear();
+    manager = mock(IdpUserGroupManager.class);
+    EventListenerPlugin listener = mock(EventListenerPlugin.class);
+    doAnswer(
+            inv -> {
+              postEvents.add(inv.getArgument(0));
+              return null;
+            })
+        .when(listener)
+        .onPostEvent(any(Event.class));
+    doAnswer(inv -> null).when(listener).onPreEvent(any(PreEvent.class));
+    dispatcher = new IdpUserGroupEventDispatcher(new 
EventBus(List.of(listener)), manager);
+  }
+
+  @Test
+  void testMutationsDispatchTypedEvents() throws Exception {
+    when(manager.addUser("alice", "Passw0rd-For-User", 
true)).thenReturn(mock(IdpUser.class));
+    when(manager.changePassword("alice", 
"Passw0rd-For-User-2")).thenReturn(true);
+    when(manager.updateEnabled("alice", false)).thenReturn(true);
+    when(manager.updateEnabled("alice", true)).thenReturn(true);
+    when(manager.removeUser("alice")).thenReturn(true);
+    when(manager.addGroup("g1", "c")).thenReturn(mock(IdpGroup.class));
+    when(manager.changeGroupMembership(eq("g1"), anyList(), isNull()))
+        .thenReturn(mock(IdpGroup.class));
+    when(manager.removeGroup("g1", true)).thenReturn(true);
+
+    asAdmin(
+        () -> {
+          dispatcher.addUser("alice", "Passw0rd-For-User", true);
+          dispatcher.changePassword("alice", "Passw0rd-For-User-2");
+          dispatcher.updateEnabled("alice", false);
+          dispatcher.updateEnabled("alice", true);
+          dispatcher.removeUser("alice");
+          dispatcher.addGroup("g1", "c");
+          dispatcher.changeGroupMembership("g1", List.of("alice"), null);
+          dispatcher.removeGroup("g1", true);
+          return null;
+        });
+
+    Assertions.assertEquals(8, postEvents.size());
+    IdpAddUserEvent addUser = (IdpAddUserEvent) postEvents.get(0);
+    Assertions.assertEquals(OperationType.ADD_USER, addUser.operationType());
+    Assertions.assertEquals("basic.user.alice", 
addUser.identifier().toString());
+    Assertions.assertEquals("admin", addUser.user());
+    Assertions.assertInstanceOf(IdpChangeUserPasswordEvent.class, 
postEvents.get(1));
+    Assertions.assertEquals(
+        OperationType.DISABLE_USER,
+        ((IdpUpdateUserEnabledEvent) postEvents.get(2)).operationType());
+    Assertions.assertEquals(
+        OperationType.ENABLE_USER, ((IdpUpdateUserEnabledEvent) 
postEvents.get(3)).operationType());
+    Assertions.assertTrue(((IdpRemoveUserEvent) postEvents.get(4)).isExists());
+    Assertions.assertInstanceOf(IdpAddGroupEvent.class, postEvents.get(5));
+    IdpChangeGroupMembershipEvent membership = (IdpChangeGroupMembershipEvent) 
postEvents.get(6);
+    Assertions.assertEquals(OperationType.ALTER_GROUP, 
membership.operationType());
+    Assertions.assertEquals(List.of("alice"), membership.usersToAdd());
+    Assertions.assertEquals("basic.group.g1", 
membership.identifier().toString());
+    Assertions.assertTrue(((IdpRemoveGroupEvent) postEvents.get(7)).force());
+  }
+
+  @Test
+  void testFailureDispatchesFailureEvent() throws Exception {
+    when(manager.addUser(eq("alice"), anyString(), eq(true)))
+        .thenThrow(new AlreadyExistsException("exists"));
+
+    Assertions.assertThrows(
+        AlreadyExistsException.class,
+        () ->
+            asAdmin(
+                () -> {
+                  dispatcher.addUser("alice", "Passw0rd-For-User", true);
+                  return null;
+                }));
+
+    Assertions.assertInstanceOf(IdpAddUserFailureEvent.class, 
postEvents.get(0));
+    Assertions.assertEquals(OperationType.ADD_USER, 
postEvents.get(0).operationType());
+  }
+
+  private static void asAdmin(PrivilegedExceptionAction<Void> action) throws 
Exception {
+    PrincipalUtils.doAs(new UserPrincipal("admin"), action);
+  }
+}
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 267726aaec..be6aa2bde4 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
@@ -56,8 +56,10 @@ import 
org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest;
 import org.apache.gravitino.idp.dto.requests.UpdateUserEnabledRequest;
 import org.apache.gravitino.idp.dto.responses.IdpGroupResponse;
 import org.apache.gravitino.idp.dto.responses.IdpUserResponse;
+import org.apache.gravitino.idp.listener.IdpUserGroupEventDispatcher;
 import org.apache.gravitino.idp.model.IdpGroup;
 import org.apache.gravitino.idp.model.IdpUser;
+import org.apache.gravitino.listener.EventBus;
 import org.apache.gravitino.rest.RESTUtils;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
@@ -74,6 +76,8 @@ class TestIdpOperations extends JerseyTest {
   private static final String ACCEPT = "application/vnd.gravitino.v1+json";
   private static final String VALID_PASSWORD = "Passw0rd-For-User";
   private static final IdpUserGroupManager MANAGER = 
mock(IdpUserGroupManager.class);
+  private static final IdpUserGroupEventDispatcher DISPATCHER =
+      new IdpUserGroupEventDispatcher(new EventBus(Collections.emptyList()), 
MANAGER);
 
   private static Config previousConfig;
 
@@ -117,7 +121,7 @@ class TestIdpOperations extends JerseyTest {
         new AbstractBinder() {
           @Override
           protected void configure() {
-            bind(MANAGER).to(IdpUserGroupManager.class);
+            bind(DISPATCHER).to(IdpUserGroupEventDispatcher.class);
             bind(request).to(HttpServletRequest.class);
           }
         });

Reply via email to