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 5d18c76932 [#12330] feat(core): Add group by-id APIs and 
alterGroupById via GroupChange (#12331)
5d18c76932 is described below

commit 5d18c769329351a98b4abceb1cfbed9236e472ab
Author: MaSai <[email protected]>
AuthorDate: Tue Aug 4 19:13:40 2026 +0800

    [#12330] feat(core): Add group by-id APIs and alterGroupById via 
GroupChange (#12331)
    
    ### What changes were proposed in this pull request?
    
    Add AccessControlDispatcher APIs that locate groups by
    Gravitino-assigned id, and a single alter path for mutable attributes.
    Builds on user by-id APIs already merged in #12332.
    
    **API / model**
    - Expose `Group.id()` and align `GroupDTO` / `GroupInfo` (required
    non-null id)
    - `getGroupById` / `removeGroupById`
    - `GroupChange` + `alterGroupById` (update `externalId`)
    
    **Persistence / store**
    - Relational lookup/update/delete by metalake + group id
    (`GroupMetaService`, mapper/SQL)
    - Wire JDBC and in-memory entity stores for `GROUP` id operations
    
    **Wiring / events**
    - `UserGroupIdManager`, `AccessControlManager`, hook dispatcher
    - Listener Pre/Event/Failure for get/remove/alter by id (`GetGroupById*`
    / `RemoveGroupById*` / `AlterGroup*`)
    - TreeLock paths via `AuthorizationUtils.ofGroupId` /
    `GROUP_ID_SCHEMA_NAME`
    
    Existing by-name get/remove and by-externalId get/remove are unchanged.
    
    Fix: #12330
    
    ### Why are the changes needed?
    
    SCIM push (Azure Entra / Okta) stores the SCIM `id` returned on create
    and uses it for subsequent `GET` / `PATCH` / `DELETE`. Core previously
    only keyed groups by name or `externalId`. A single `alterGroupById`
    matches SCIM PATCH that may update `externalId`.
    
    ### Does this PR introduce _any_ user-facing change?
    
    - Yes: new `Group.id()`, by-id get/remove, `GroupChange` /
    `alterGroupById`, and matching listener events / `OperationType` values
    - Existing by-name and by-externalId group APIs unchanged
    
    ### How was this patch tested?
    
    ```bash
    ./gradlew :api:test \
      --tests org.apache.gravitino.authorization.TestGroupChange \
      :common:test \
      --tests org.apache.gravitino.dto.responses.TestResponses \
      :core:test \
      --tests org.apache.gravitino.authorization.TestAccessControlManager \
      --tests org.apache.gravitino.listener.api.event.TestGroupEvent \
      :clients:client-java:test \
      --tests org.apache.gravitino.client.TestUserGroup \
      --tests org.apache.gravitino.client.TestPermission \
      -PskipITs
    ```
    
    ---------
    
    Co-authored-by: Cursor <[email protected]>
---
 .../org/apache/gravitino/authorization/Group.java  | 13 +++-
 .../gravitino/authorization/GroupChange.java       | 82 +++++++++++++++++++
 .../gravitino/authorization/TestGroupChange.java   | 35 +++++++++
 .../apache/gravitino/cli/commands/ListGroups.java  |  5 ++
 .../apache/gravitino/client/TestPermission.java    |  2 +
 .../org/apache/gravitino/client/TestUserGroup.java |  2 +
 .../gravitino/dto/authorization/GroupDTO.java      | 32 +++++++-
 .../apache/gravitino/dto/util/DTOConverters.java   |  1 +
 .../gravitino/dto/responses/TestResponses.java     |  2 +-
 .../src/main/java/org/apache/gravitino/Entity.java | 10 +++
 .../java/org/apache/gravitino/audit/AuditLog.java  |  6 ++
 .../gravitino/audit/v2/CompatibilityUtils.java     |  3 +
 .../authorization/AccessControlDispatcher.java     | 43 ++++++++++
 .../authorization/AccessControlManager.java        | 26 +++++++
 .../authorization/AuthorizationUtils.java          | 39 ++++++++++
 .../authorization/UserGroupIdManager.java          | 87 +++++++++++++++++++++
 .../hook/AccessControlHookDispatcher.java          | 18 +++++
 .../listener/AccessControlEventDispatcher.java     | 65 ++++++++++++++++
 .../listener/api/event/AlterGroupEvent.java        | 72 +++++++++++++++++
 .../listener/api/event/AlterGroupFailureEvent.java | 70 +++++++++++++++++
 .../listener/api/event/AlterGroupPreEvent.java     | 69 ++++++++++++++++
 .../listener/api/event/GetGroupByIdEvent.java      | 56 +++++++++++++
 .../api/event/GetGroupByIdFailureEvent.java        | 57 ++++++++++++++
 .../listener/api/event/GetGroupByIdPreEvent.java   | 55 +++++++++++++
 .../listener/api/event/OperationType.java          |  3 +
 .../listener/api/event/RemoveGroupByIdEvent.java   | 67 ++++++++++++++++
 .../api/event/RemoveGroupByIdFailureEvent.java     | 57 ++++++++++++++
 .../api/event/RemoveGroupByIdPreEvent.java         | 55 +++++++++++++
 .../gravitino/listener/api/info/GroupInfo.java     | 14 +++-
 .../gravitino/storage/relational/JDBCBackend.java  | 10 +++
 .../storage/relational/mapper/GroupMetaMapper.java |  6 ++
 .../mapper/GroupMetaSQLProviderFactory.java        |  5 ++
 .../provider/base/GroupMetaBaseSQLProvider.java    | 17 ++++
 .../relational/service/GroupMetaService.java       | 91 ++++++++++++++++++++++
 .../gravitino/audit/v2/TestCompatibilityUtils.java |  3 +
 .../authorization/TestAccessControlManager.java    | 35 +++++++++
 .../listener/api/event/TestGroupEvent.java         | 76 ++++++++++++++++++
 .../storage/memory/TestMemoryEntityStore.java      | 19 ++++-
 38 files changed, 1299 insertions(+), 9 deletions(-)

diff --git a/api/src/main/java/org/apache/gravitino/authorization/Group.java 
b/api/src/main/java/org/apache/gravitino/authorization/Group.java
index 307380d4b9..f97ae948f0 100644
--- a/api/src/main/java/org/apache/gravitino/authorization/Group.java
+++ b/api/src/main/java/org/apache/gravitino/authorization/Group.java
@@ -34,13 +34,24 @@ public interface Group extends Auditable {
    */
   String name();
 
+  /**
+   * The unique id assigned by Gravitino.
+   *
+   * <p>This id is server-assigned and immutable. Upstream systems may also 
supply an optional
+   * {@link #externalId()}.
+   *
+   * @return The unique id of the group.
+   */
+  Long id();
+
   /**
    * The stable identifier assigned by an upstream identity system (for 
example, SCIM, LDAP, or
    * IAM), or null if not set.
    *
    * <p>Gravitino {@link Group#name() group names} may differ from upstream 
ids or be unknown at
    * sync time. External id lets integrators look up and delete groups without 
relying on the
-   * Gravitino group name.
+   * Gravitino group name. Mutable attributes such as {@code externalId} are 
updated via {@code
+   * alterGroupById}.
    *
    * @return The upstream external identifier, or null if not set.
    */
diff --git 
a/api/src/main/java/org/apache/gravitino/authorization/GroupChange.java 
b/api/src/main/java/org/apache/gravitino/authorization/GroupChange.java
new file mode 100644
index 0000000000..af9f62e873
--- /dev/null
+++ b/api/src/main/java/org/apache/gravitino/authorization/GroupChange.java
@@ -0,0 +1,82 @@
+/*
+ * 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.authorization;
+
+import java.util.Objects;
+import javax.annotation.Nullable;
+import org.apache.gravitino.annotation.Evolving;
+
+/**
+ * A group change is a change to a group. It can be used to update the 
optional external id.
+ * Multiple changes may be applied in one {@code alterGroupById} call.
+ */
+@Evolving
+public interface GroupChange {
+
+  /**
+   * Creates a group change to update the external identifier.
+   *
+   * @param newExternalId The new external identifier, or null to clear it.
+   * @return The group change.
+   */
+  static GroupChange updateExternalId(@Nullable String newExternalId) {
+    return new UpdateExternalId(newExternalId);
+  }
+
+  /** A group change to update the external identifier. */
+  final class UpdateExternalId implements GroupChange {
+    @Nullable private final String newExternalId;
+
+    private UpdateExternalId(@Nullable String newExternalId) {
+      this.newExternalId = newExternalId;
+    }
+
+    /**
+     * Returns the new external identifier, or null to clear it.
+     *
+     * @return The new external identifier, or null.
+     */
+    @Nullable
+    public String getNewExternalId() {
+      return newExternalId;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      if (this == o) {
+        return true;
+      }
+      if (!(o instanceof UpdateExternalId)) {
+        return false;
+      }
+      UpdateExternalId that = (UpdateExternalId) o;
+      return Objects.equals(newExternalId, that.newExternalId);
+    }
+
+    @Override
+    public int hashCode() {
+      return Objects.hash(newExternalId);
+    }
+
+    @Override
+    public String toString() {
+      return "UpdateExternalId " + newExternalId;
+    }
+  }
+}
diff --git 
a/api/src/test/java/org/apache/gravitino/authorization/TestGroupChange.java 
b/api/src/test/java/org/apache/gravitino/authorization/TestGroupChange.java
new file mode 100644
index 0000000000..a029befe86
--- /dev/null
+++ b/api/src/test/java/org/apache/gravitino/authorization/TestGroupChange.java
@@ -0,0 +1,35 @@
+/*
+ * 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.authorization;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestGroupChange {
+
+  @Test
+  void testUpdateExternalId() {
+    GroupChange.UpdateExternalId change =
+        (GroupChange.UpdateExternalId) GroupChange.updateExternalId("ext-1");
+    Assertions.assertEquals("ext-1", change.getNewExternalId());
+    Assertions.assertEquals(GroupChange.updateExternalId("ext-1"), change);
+    Assertions.assertEquals(GroupChange.updateExternalId(null), 
GroupChange.updateExternalId(null));
+    Assertions.assertNotEquals(GroupChange.updateExternalId("ext-2"), change);
+  }
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
index 588f30393e..8d84614702 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
@@ -68,6 +68,11 @@ public class ListGroups extends Command {
 
   private Group getGroup(String name) {
     return new Group() {
+      @Override
+      public Long id() {
+        return null;
+      }
+
       @Override
       public String name() {
         return name;
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestPermission.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestPermission.java
index 4704886190..50c529ed8a 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestPermission.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestPermission.java
@@ -148,6 +148,7 @@ public class TestPermission extends TestBase {
     RoleGrantRequest request = new RoleGrantRequest(roles);
     GroupDTO groupDTO =
         GroupDTO.builder()
+            .withId(1L)
             .withName("group")
             .withRoles(Lists.newArrayList("roles"))
             
.withAudit(AuditDTO.builder().withCreator("test").withCreateTime(Instant.now()).build())
@@ -174,6 +175,7 @@ public class TestPermission extends TestBase {
         String.format(API_PERMISSION_PATH, metalakeName, 
String.format("groups/%s/revoke", group));
     GroupDTO groupDTO =
         GroupDTO.builder()
+            .withId(1L)
             .withName("group")
             .withRoles(Lists.newArrayList())
             
.withAudit(AuditDTO.builder().withCreator("test").withCreateTime(Instant.now()).build())
diff --git 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
index 9c0a12d133..dbd7aab14e 100644
--- 
a/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
+++ 
b/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java
@@ -306,6 +306,7 @@ public class TestUserGroup extends TestBase {
 
     GroupDTO mockGroup =
         GroupDTO.builder()
+            .withId(1L)
             .withName(groupName)
             .withExternalId(externalId)
             .withAudit(
@@ -436,6 +437,7 @@ public class TestUserGroup extends TestBase {
 
   private GroupDTO mockGroupDTO(String name) {
     return GroupDTO.builder()
+        .withId(1L)
         .withName(name)
         
.withAudit(AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build())
         .build();
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java 
b/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java
index 61fbc814df..7d34ee2c35 100644
--- a/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java
+++ b/common/src/main/java/org/apache/gravitino/dto/authorization/GroupDTO.java
@@ -31,6 +31,9 @@ import org.apache.gravitino.dto.AuditDTO;
 /** Represents a Group Data Transfer Object (DTO). */
 public class GroupDTO implements Group {
 
+  @JsonProperty("id")
+  private Long id;
+
   @JsonProperty("name")
   private String name;
 
@@ -50,18 +53,28 @@ public class GroupDTO implements Group {
   /**
    * Creates a new instance of GroupDTO.
    *
+   * @param id The id of the Group DTO.
    * @param name The name of the Group DTO.
    * @param externalId The external id of the Group DTO.
    * @param roles The roles of the Group DTO.
    * @param audit The audit information of the Group DTO.
    */
-  protected GroupDTO(String name, String externalId, List<String> roles, 
AuditDTO audit) {
+  protected GroupDTO(Long id, String name, String externalId, List<String> 
roles, AuditDTO audit) {
+    this.id = id;
     this.name = name;
     this.externalId = externalId;
     this.audit = audit;
     this.roles = roles;
   }
 
+  /**
+   * @return The id of the Group DTO.
+   */
+  @Override
+  public Long id() {
+    return id;
+  }
+
   /**
    * @return The name of the Group DTO.
    */
@@ -109,6 +122,9 @@ public class GroupDTO implements Group {
    */
   public static class Builder<S extends Builder> {
 
+    /** The id of the group. */
+    protected Long id;
+
     /** The name of the group. */
     protected String name;
 
@@ -121,6 +137,17 @@ public class GroupDTO implements Group {
     /** The audit information of the group. */
     protected AuditDTO audit;
 
+    /**
+     * Sets the id of the group.
+     *
+     * @param id The id of the group.
+     * @return The builder instance.
+     */
+    public S withId(Long id) {
+      this.id = id;
+      return (S) this;
+    }
+
     /**
      * Sets the name of the group.
      *
@@ -175,9 +202,10 @@ public class GroupDTO implements Group {
      * @throws IllegalArgumentException If the name or audit are not set.
      */
     public GroupDTO build() {
+      Preconditions.checkArgument(id != null, "id cannot be null");
       Preconditions.checkArgument(StringUtils.isNotBlank(name), "name cannot 
be null or empty");
       Preconditions.checkArgument(audit != null, "audit cannot be null");
-      return new GroupDTO(name, externalId, roles, audit);
+      return new GroupDTO(id, name, externalId, roles, audit);
     }
   }
 }
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java 
b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
index 75394cc69f..864823a77b 100644
--- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
+++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java
@@ -514,6 +514,7 @@ public class DTOConverters {
     }
 
     return GroupDTO.builder()
+        .withId(group.id())
         .withName(group.name())
         .withExternalId(group.externalId())
         .withRoles(group.roles())
diff --git 
a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java 
b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
index 67f2c43659..85a321c103 100644
--- a/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
+++ b/common/src/test/java/org/apache/gravitino/dto/responses/TestResponses.java
@@ -310,7 +310,7 @@ public class TestResponses {
   void testGroupResponse() throws IllegalArgumentException {
     AuditDTO audit =
         
AuditDTO.builder().withCreator("creator").withCreateTime(Instant.now()).build();
-    GroupDTO group = 
GroupDTO.builder().withName("group1").withAudit(audit).build();
+    GroupDTO group = 
GroupDTO.builder().withId(1L).withName("group1").withAudit(audit).build();
     GroupResponse response = new GroupResponse(group);
     response.validate(); // No exception thrown
   }
diff --git a/core/src/main/java/org/apache/gravitino/Entity.java 
b/core/src/main/java/org/apache/gravitino/Entity.java
index 934f119eb0..5c214cb434 100644
--- a/core/src/main/java/org/apache/gravitino/Entity.java
+++ b/core/src/main/java/org/apache/gravitino/Entity.java
@@ -65,6 +65,16 @@ public interface Entity extends Serializable {
   /** The group schema name in the system catalog. */
   String GROUP_SCHEMA_NAME = "group";
 
+  /**
+   * A virtual schema name used only for {@link 
org.apache.gravitino.lock.TreeLockUtils} lock paths
+   * when operating on groups by Gravitino-assigned id (for example, 
get/alter/delete-by-id).
+   *
+   * <p>This is not a real metadata schema and does not store entities. It 
forms part of a synthetic
+   * {@link org.apache.gravitino.NameIdentifier} such as {@code {metalake, 
system, group-id, <id>}}
+   * so that concurrent operations on the same group id are serialized.
+   */
+  String GROUP_ID_SCHEMA_NAME = "group-id";
+
   /**
    * A virtual schema name used only for {@link 
org.apache.gravitino.lock.TreeLockUtils} lock paths
    * when operating on groups by external id (for example, 
get/delete-by-external-id).
diff --git a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java 
b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
index 0b431b52f5..6a91a41b24 100644
--- a/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
+++ b/core/src/main/java/org/apache/gravitino/audit/AuditLog.java
@@ -406,6 +406,12 @@ public interface AuditLog {
 
     GET_GROUP_BY_EXTERNAL_ID,
 
+    GET_GROUP_BY_ID,
+
+    REMOVE_GROUP_BY_ID,
+
+    ALTER_GROUP,
+
     LIST_GROUPS,
 
     LIST_GROUP_NAMES,
diff --git 
a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java 
b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
index 53337d0a82..3e254400ef 100644
--- a/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
+++ b/core/src/main/java/org/apache/gravitino/audit/v2/CompatibilityUtils.java
@@ -134,6 +134,9 @@ public class CompatibilityUtils {
           .put(OperationType.REMOVE_GROUP_BY_EXTERNAL_ID, 
Operation.REMOVE_GROUP_BY_EXTERNAL_ID)
           .put(OperationType.GET_GROUP, Operation.GET_GROUP)
           .put(OperationType.GET_GROUP_BY_EXTERNAL_ID, 
Operation.GET_GROUP_BY_EXTERNAL_ID)
+          .put(OperationType.GET_GROUP_BY_ID, Operation.GET_GROUP_BY_ID)
+          .put(OperationType.REMOVE_GROUP_BY_ID, Operation.REMOVE_GROUP_BY_ID)
+          .put(OperationType.ALTER_GROUP, Operation.ALTER_GROUP)
           .put(OperationType.LIST_GROUPS, Operation.LIST_GROUPS)
           .put(OperationType.LIST_GROUP_NAMES, Operation.LIST_GROUP_NAMES)
           .put(OperationType.GRANT_GROUP_ROLES, Operation.GRANT_GROUP_ROLES)
diff --git 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
index 20c3c0ba0d..5f0a1bd462 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
@@ -260,6 +260,49 @@ public interface AccessControlDispatcher {
   Group getGroupByExternalId(String metalake, String externalId)
       throws NoSuchGroupException, NoSuchMetalakeException;
 
+  /**
+   * Gets a Group by Gravitino-assigned id.
+   *
+   * @param metalake The Metalake of the Group.
+   * @param groupId The Gravitino-assigned id of the Group.
+   * @return The getting Group instance.
+   * @throws NoSuchGroupException If the Group with the given id does not 
exist.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   * @throws RuntimeException If getting the Group encounters storage issues.
+   */
+  Group getGroupById(String metalake, long groupId)
+      throws NoSuchGroupException, NoSuchMetalakeException;
+
+  /**
+   * Removes a Group by Gravitino-assigned id.
+   *
+   * @param metalake The Metalake of the Group.
+   * @param groupId The Gravitino-assigned id of the Group.
+   * @return True if the Group was successfully removed, false only when 
there's no such group,
+   *     otherwise it will throw an exception.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   * @throws RuntimeException If removing the Group encounters storage issues.
+   */
+  boolean removeGroupById(String metalake, long groupId) throws 
NoSuchMetalakeException;
+
+  /**
+   * Alters a Group by Gravitino-assigned id.
+   *
+   * <p>Supports updating {@code externalId} via {@link GroupChange}. Role 
bindings are preserved.
+   *
+   * @param metalake The Metalake of the Group.
+   * @param groupId The Gravitino-assigned id of the Group.
+   * @param changes The changes to apply. Must not be empty.
+   * @return The updated Group instance.
+   * @throws IllegalArgumentException If changes is null or empty, or contains 
an unsupported
+   *     change.
+   * @throws NoSuchGroupException If the Group with the given id does not 
exist.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   * @throws RuntimeException If updating the Group encounters storage issues.
+   */
+  Group alterGroupById(String metalake, long groupId, GroupChange... changes)
+      throws NoSuchGroupException, NoSuchMetalakeException;
+
   /**
    * List groups
    *
diff --git 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
index 386e7a3936..36cd4443b9 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
@@ -210,6 +210,32 @@ public class AccessControlManager implements 
AccessControlDispatcher {
         () -> userGroupExternalManager.getGroupByExternalId(metalake, 
externalId));
   }
 
+  @Override
+  public Group getGroupById(String metalake, long groupId)
+      throws NoSuchGroupException, NoSuchMetalakeException {
+    return TreeLockUtils.doWithTreeLock(
+        AuthorizationUtils.ofGroupId(metalake, groupId),
+        LockType.READ,
+        () -> userGroupIdManager.getGroupById(metalake, groupId));
+  }
+
+  @Override
+  public boolean removeGroupById(String metalake, long groupId) throws 
NoSuchMetalakeException {
+    return TreeLockUtils.doWithTreeLock(
+        AuthorizationUtils.ofGroupId(metalake, groupId),
+        LockType.WRITE,
+        () -> userGroupIdManager.removeGroupById(metalake, groupId));
+  }
+
+  @Override
+  public Group alterGroupById(String metalake, long groupId, GroupChange... 
changes)
+      throws NoSuchGroupException, NoSuchMetalakeException {
+    return TreeLockUtils.doWithTreeLock(
+        AuthorizationUtils.ofGroupId(metalake, groupId),
+        LockType.WRITE,
+        () -> userGroupIdManager.alterGroupById(metalake, groupId, changes));
+  }
+
   @Override
   public Group[] listGroups(String metalake) throws NoSuchMetalakeException {
     return TreeLockUtils.doWithTreeLock(
diff --git 
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java 
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
index 8c555cf29c..76f77770e8 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AuthorizationUtils.java
@@ -71,6 +71,8 @@ public class AuthorizationUtils {
   static final String GROUP_DOES_NOT_EXIST_MSG = "Group %s does not exist in 
the metalake %s";
   static final String GROUP_WITH_EXTERNAL_ID_DOES_NOT_EXIST_MSG =
       "Group with external id %s does not exist in the metalake %s";
+  static final String GROUP_WITH_ID_DOES_NOT_EXIST_MSG =
+      "Group with id %s does not exist in the metalake %s";
   static final String ROLE_DOES_NOT_EXIST_MSG = "Role %s does not exist in the 
metalake %s";
 
   /**
@@ -183,6 +185,22 @@ public class AuthorizationUtils {
         String.valueOf(userId));
   }
 
+  /**
+   * Creates a synthetic {@link NameIdentifier} used only as a tree-lock path 
for group operations
+   * keyed by Gravitino-assigned id.
+   *
+   * @param metalake the metalake name
+   * @param groupId the Gravitino-assigned group id
+   * @return a synthetic name identifier for tree locking only
+   */
+  public static NameIdentifier ofGroupId(String metalake, long groupId) {
+    return NameIdentifier.of(
+        metalake,
+        Entity.SYSTEM_CATALOG_RESERVED_NAME,
+        Entity.GROUP_ID_SCHEMA_NAME,
+        String.valueOf(groupId));
+  }
+
   /**
    * Creates a synthetic {@link NameIdentifier} used only as a {@link
    * org.apache.gravitino.lock.TreeLockUtils} lock path for group operations 
keyed by external id.
@@ -229,6 +247,10 @@ public class AuthorizationUtils {
         metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME, 
Entity.GROUP_EXTERNAL_ID_SCHEMA_NAME);
   }
 
+  public static Namespace ofGroupIdNamespace(String metalake) {
+    return Namespace.of(metalake, Entity.SYSTEM_CATALOG_RESERVED_NAME, 
Entity.GROUP_ID_SCHEMA_NAME);
+  }
+
   public static void checkUser(NameIdentifier ident) {
     NameIdentifier.check(ident != null, "User identifier must not be null");
     checkUserNamespace(ident.namespace());
@@ -269,6 +291,16 @@ public class AuthorizationUtils {
     checkGroupExternalIdNamespace(ident.namespace());
   }
 
+  /**
+   * Validates that the name identifier refers to a group id in a metalake.
+   *
+   * @param ident the group id name identifier to validate
+   */
+  public static void checkGroupId(NameIdentifier ident) {
+    NameIdentifier.check(ident != null, "Group id identifier must not be 
null");
+    checkGroupIdNamespace(ident.namespace());
+  }
+
   public static void checkRole(NameIdentifier ident) {
     NameIdentifier.check(ident != null, "Role identifier must not be null");
     checkRoleNamespace(ident.namespace());
@@ -302,6 +334,13 @@ public class AuthorizationUtils {
         namespace);
   }
 
+  public static void checkGroupIdNamespace(Namespace namespace) {
+    Namespace.check(
+        namespace != null && namespace.length() == 3,
+        "Group id namespace must have 3 levels, the input namespace is %s",
+        namespace);
+  }
+
   public static void checkGroupNamespace(Namespace namespace) {
     Namespace.check(
         namespace != null && namespace.length() == 3,
diff --git 
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupIdManager.java 
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupIdManager.java
index 22001934b7..6583a73499 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupIdManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupIdManager.java
@@ -24,8 +24,10 @@ import java.time.Instant;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
+import org.apache.gravitino.exceptions.NoSuchGroupException;
 import org.apache.gravitino.exceptions.NoSuchUserException;
 import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.GroupEntity;
 import org.apache.gravitino.meta.UserEntity;
 import org.apache.gravitino.storage.IdGenerator;
 import org.apache.gravitino.utils.PrincipalUtils;
@@ -135,4 +137,89 @@ class UserGroupIdManager extends UserGroupManager {
                 .build())
         .build();
   }
+
+  boolean removeGroupById(String metalake, long groupId) {
+    try {
+      return store
+          .idOperations()
+          .deleteById(AuthorizationUtils.ofGroupId(metalake, groupId), 
Entity.EntityType.GROUP);
+    } catch (IOException ioe) {
+      LOG.error(
+          "Removing group with id {} in the metalake {} failed due to storage 
issues",
+          groupId,
+          metalake,
+          ioe);
+      throw new RuntimeException(ioe);
+    }
+  }
+
+  Group getGroupById(String metalake, long groupId) throws 
NoSuchGroupException {
+    try {
+      return store
+          .idOperations()
+          .getById(
+              AuthorizationUtils.ofGroupId(metalake, groupId),
+              Entity.EntityType.GROUP,
+              GroupEntity.class);
+    } catch (NoSuchEntityException e) {
+      LOG.warn("Group with id {} does not exist in the metalake {}", groupId, 
metalake, e);
+      throw new NoSuchGroupException(
+          AuthorizationUtils.GROUP_WITH_ID_DOES_NOT_EXIST_MSG, groupId, 
metalake);
+    } catch (IOException ioe) {
+      LOG.error("Getting group with id {} failed due to storage issues", 
groupId, ioe);
+      throw new RuntimeException(ioe);
+    }
+  }
+
+  Group alterGroupById(String metalake, long groupId, GroupChange... changes)
+      throws NoSuchGroupException {
+    Preconditions.checkArgument(
+        changes != null && changes.length > 0, "Group changes cannot be 
empty");
+    try {
+      return store
+          .idOperations()
+          .updateById(
+              AuthorizationUtils.ofGroupId(metalake, groupId),
+              Entity.EntityType.GROUP,
+              GroupEntity.class,
+              group -> applyGroupChanges(group, changes));
+    } catch (NoSuchEntityException e) {
+      LOG.warn("Group with id {} does not exist in the metalake {}", groupId, 
metalake, e);
+      throw new NoSuchGroupException(
+          AuthorizationUtils.GROUP_WITH_ID_DOES_NOT_EXIST_MSG, groupId, 
metalake);
+    } catch (IOException ioe) {
+      LOG.error(
+          "Altering group with id {} in the metalake {} failed due to storage 
issues",
+          groupId,
+          metalake,
+          ioe);
+      throw new RuntimeException(ioe);
+    }
+  }
+
+  private static GroupEntity applyGroupChanges(GroupEntity group, 
GroupChange... changes) {
+    String externalId = group.externalId();
+    for (GroupChange change : changes) {
+      if (change instanceof GroupChange.UpdateExternalId) {
+        externalId = ((GroupChange.UpdateExternalId) 
change).getNewExternalId();
+      } else {
+        throw new IllegalArgumentException("Unsupported group change: " + 
change);
+      }
+    }
+    return GroupEntity.builder()
+        .withId(group.id())
+        .withName(group.name())
+        .withNamespace(group.namespace())
+        .withExternalId(externalId)
+        .withRoleNames(group.roleNames())
+        .withRoleIds(group.roleIds())
+        .withAuditInfo(
+            AuditInfo.builder()
+                .withCreator(group.auditInfo().creator())
+                .withCreateTime(group.auditInfo().createTime())
+                
.withLastModifier(PrincipalUtils.getCurrentPrincipal().getName())
+                .withLastModifiedTime(Instant.now())
+                .build())
+        .build();
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java 
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
index f8028b8c3a..92c6f47f1b 100644
--- 
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
@@ -28,6 +28,7 @@ import 
org.apache.gravitino.authorization.AccessControlDispatcher;
 import org.apache.gravitino.authorization.AuthorizationUtils;
 import org.apache.gravitino.authorization.GravitinoAuthorizer;
 import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.GroupChange;
 import org.apache.gravitino.authorization.Owner;
 import org.apache.gravitino.authorization.OwnerDispatcher;
 import org.apache.gravitino.authorization.Privilege;
@@ -161,6 +162,23 @@ public class AccessControlHookDispatcher implements 
AccessControlDispatcher {
     return dispatcher.getGroupByExternalId(metalake, externalId);
   }
 
+  @Override
+  public Group getGroupById(String metalake, long groupId)
+      throws NoSuchGroupException, NoSuchMetalakeException {
+    return dispatcher.getGroupById(metalake, groupId);
+  }
+
+  @Override
+  public boolean removeGroupById(String metalake, long groupId) throws 
NoSuchMetalakeException {
+    return dispatcher.removeGroupById(metalake, groupId);
+  }
+
+  @Override
+  public Group alterGroupById(String metalake, long groupId, GroupChange... 
changes)
+      throws NoSuchGroupException, NoSuchMetalakeException {
+    return dispatcher.alterGroupById(metalake, groupId, changes);
+  }
+
   @Override
   public Group[] listGroups(String metalake) throws NoSuchMetalakeException {
     return dispatcher.listGroups(metalake);
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
 
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
index dd3f734402..c52406a4e6 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
@@ -25,6 +25,7 @@ import java.util.Set;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.authorization.AccessControlDispatcher;
 import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.GroupChange;
 import org.apache.gravitino.authorization.Privilege;
 import org.apache.gravitino.authorization.Role;
 import org.apache.gravitino.authorization.SecurableObject;
@@ -45,6 +46,9 @@ import 
org.apache.gravitino.listener.api.event.AddGroupPreEvent;
 import org.apache.gravitino.listener.api.event.AddUserEvent;
 import org.apache.gravitino.listener.api.event.AddUserFailureEvent;
 import org.apache.gravitino.listener.api.event.AddUserPreEvent;
+import org.apache.gravitino.listener.api.event.AlterGroupEvent;
+import org.apache.gravitino.listener.api.event.AlterGroupFailureEvent;
+import org.apache.gravitino.listener.api.event.AlterGroupPreEvent;
 import org.apache.gravitino.listener.api.event.CreateRoleEvent;
 import org.apache.gravitino.listener.api.event.CreateRoleFailureEvent;
 import org.apache.gravitino.listener.api.event.CreateRolePreEvent;
@@ -54,6 +58,9 @@ import 
org.apache.gravitino.listener.api.event.DeleteRolePreEvent;
 import org.apache.gravitino.listener.api.event.GetGroupByExternalIdEvent;
 import 
org.apache.gravitino.listener.api.event.GetGroupByExternalIdFailureEvent;
 import org.apache.gravitino.listener.api.event.GetGroupByExternalIdPreEvent;
+import org.apache.gravitino.listener.api.event.GetGroupByIdEvent;
+import org.apache.gravitino.listener.api.event.GetGroupByIdFailureEvent;
+import org.apache.gravitino.listener.api.event.GetGroupByIdPreEvent;
 import org.apache.gravitino.listener.api.event.GetGroupEvent;
 import org.apache.gravitino.listener.api.event.GetGroupFailureEvent;
 import org.apache.gravitino.listener.api.event.GetGroupPreEvent;
@@ -96,6 +103,9 @@ import 
org.apache.gravitino.listener.api.event.OverridePrivilegesPreEvent;
 import org.apache.gravitino.listener.api.event.RemoveGroupByExternalIdEvent;
 import 
org.apache.gravitino.listener.api.event.RemoveGroupByExternalIdFailureEvent;
 import org.apache.gravitino.listener.api.event.RemoveGroupByExternalIdPreEvent;
+import org.apache.gravitino.listener.api.event.RemoveGroupByIdEvent;
+import org.apache.gravitino.listener.api.event.RemoveGroupByIdFailureEvent;
+import org.apache.gravitino.listener.api.event.RemoveGroupByIdPreEvent;
 import org.apache.gravitino.listener.api.event.RemoveGroupEvent;
 import org.apache.gravitino.listener.api.event.RemoveGroupFailureEvent;
 import org.apache.gravitino.listener.api.event.RemoveGroupPreEvent;
@@ -418,6 +428,61 @@ public class AccessControlEventDispatcher implements 
AccessControlDispatcher {
     }
   }
 
+  /** {@inheritDoc} */
+  @Override
+  public Group getGroupById(String metalake, long groupId)
+      throws NoSuchGroupException, NoSuchMetalakeException {
+    String initiator = PrincipalUtils.getCurrentUserName();
+
+    eventBus.dispatchEvent(new GetGroupByIdPreEvent(initiator, metalake, 
groupId));
+    try {
+      Group groupObject = dispatcher.getGroupById(metalake, groupId);
+      eventBus.dispatchEvent(
+          new GetGroupByIdEvent(initiator, metalake, new 
GroupInfo(groupObject)));
+
+      return groupObject;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new GetGroupByIdFailureEvent(initiator, metalake, 
e, groupId));
+      throw e;
+    }
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public boolean removeGroupById(String metalake, long groupId) throws 
NoSuchMetalakeException {
+    String initiator = PrincipalUtils.getCurrentUserName();
+
+    eventBus.dispatchEvent(new RemoveGroupByIdPreEvent(initiator, metalake, 
groupId));
+    try {
+      boolean isExists = dispatcher.removeGroupById(metalake, groupId);
+      eventBus.dispatchEvent(new RemoveGroupByIdEvent(initiator, metalake, 
groupId, isExists));
+
+      return isExists;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new RemoveGroupByIdFailureEvent(initiator, 
metalake, e, groupId));
+      throw e;
+    }
+  }
+
+  /** {@inheritDoc} */
+  @Override
+  public Group alterGroupById(String metalake, long groupId, GroupChange... 
changes)
+      throws NoSuchGroupException, NoSuchMetalakeException {
+    String initiator = PrincipalUtils.getCurrentUserName();
+
+    eventBus.dispatchEvent(new AlterGroupPreEvent(initiator, metalake, 
groupId, changes));
+    try {
+      Group groupObject = dispatcher.alterGroupById(metalake, groupId, 
changes);
+      eventBus.dispatchEvent(
+          new AlterGroupEvent(initiator, metalake, changes, new 
GroupInfo(groupObject)));
+
+      return groupObject;
+    } catch (Exception e) {
+      eventBus.dispatchEvent(new AlterGroupFailureEvent(initiator, metalake, 
groupId, changes, e));
+      throw e;
+    }
+  }
+
   /** {@inheritDoc} */
   @Override
   public Group[] listGroups(String metalake) {
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupEvent.java
new file mode 100644
index 0000000000..7e9d4922e6
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupEvent.java
@@ -0,0 +1,72 @@
+/*
+ * 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.listener.api.event;
+
+import javax.annotation.Nullable;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.GroupChange;
+import org.apache.gravitino.listener.api.info.GroupInfo;
+
+/** Represents an event triggered upon the successful alteration of a group. */
+@DeveloperApi
+public final class AlterGroupEvent extends GroupEvent {
+  private final GroupInfo updatedGroupInfo;
+  private final GroupChange[] groupChanges;
+
+  /**
+   * Creates a new {@link AlterGroupEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param groupChanges The changes applied to the group.
+   * @param updatedGroupInfo The post-alteration state of the group.
+   */
+  public AlterGroupEvent(
+      String initiator, String metalake, GroupChange[] groupChanges, GroupInfo 
updatedGroupInfo) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, 
updatedGroupInfo.id()));
+    this.groupChanges = groupChanges != null ? groupChanges.clone() : null;
+    this.updatedGroupInfo = updatedGroupInfo;
+  }
+
+  /**
+   * Returns the final state of the group after successful alteration.
+   *
+   * @return The updated group information.
+   */
+  public GroupInfo updatedGroupInfo() {
+    return updatedGroupInfo;
+  }
+
+  /**
+   * Returns the specific changes that were made to the group.
+   *
+   * @return An array of {@link GroupChange}, or null.
+   */
+  @Nullable
+  public GroupChange[] changes() {
+    return groupChanges;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.ALTER_GROUP;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupFailureEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupFailureEvent.java
new file mode 100644
index 0000000000..790bfc8746
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupFailureEvent.java
@@ -0,0 +1,70 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.GroupChange;
+
+/** Represents an event triggered when an attempt to alter a group fails due 
to an exception. */
+@DeveloperApi
+public class AlterGroupFailureEvent extends GroupFailureEvent {
+  private final long groupId;
+  private final GroupChange[] changes;
+
+  /**
+   * Creates a new {@link AlterGroupFailureEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param groupId The Gravitino-assigned id of the group.
+   * @param changes The changes attempted to be made to the group.
+   * @param exception The exception that caused the failure.
+   */
+  public AlterGroupFailureEvent(
+      String initiator, String metalake, long groupId, GroupChange[] changes, 
Exception exception) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, groupId), 
exception);
+    this.groupId = groupId;
+    this.changes = changes;
+  }
+
+  /**
+   * Returns the Gravitino-assigned id of the group.
+   *
+   * @return The group id.
+   */
+  public long groupId() {
+    return groupId;
+  }
+
+  /**
+   * Returns the changes attempted to be made to the group.
+   *
+   * @return An array of {@link GroupChange}.
+   */
+  public GroupChange[] changes() {
+    return changes;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.ALTER_GROUP;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupPreEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupPreEvent.java
new file mode 100644
index 0000000000..322c7422a6
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/AlterGroupPreEvent.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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.GroupChange;
+
+/** Represents an event triggered before altering a group by 
Gravitino-assigned id. */
+@DeveloperApi
+public class AlterGroupPreEvent extends GroupPreEvent {
+  private final long groupId;
+  private final GroupChange[] changes;
+
+  /**
+   * Creates a new {@link AlterGroupPreEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param groupId The Gravitino-assigned id of the group.
+   * @param changes The changes being applied to the group.
+   */
+  public AlterGroupPreEvent(
+      String initiator, String metalake, long groupId, GroupChange[] changes) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, groupId));
+    this.groupId = groupId;
+    this.changes = changes;
+  }
+
+  /**
+   * Returns the Gravitino-assigned id of the group being altered.
+   *
+   * @return The group id.
+   */
+  public long groupId() {
+    return groupId;
+  }
+
+  /**
+   * Returns the changes being applied to the group.
+   *
+   * @return An array of {@link GroupChange}.
+   */
+  public GroupChange[] changes() {
+    return changes;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.ALTER_GROUP;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdEvent.java
new file mode 100644
index 0000000000..4e7087c04b
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdEvent.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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.listener.api.info.GroupInfo;
+
+/** Represents an event triggered after successfully retrieving a group by 
Gravitino-assigned id. */
+@DeveloperApi
+public class GetGroupByIdEvent extends GroupEvent {
+  private final GroupInfo loadedGroupInfo;
+
+  /**
+   * Creates a new {@link GetGroupByIdEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param loadedGroupInfo The retrieved group information.
+   */
+  public GetGroupByIdEvent(String initiator, String metalake, GroupInfo 
loadedGroupInfo) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, 
loadedGroupInfo.id()));
+    this.loadedGroupInfo = loadedGroupInfo;
+  }
+
+  /**
+   * Returns the retrieved group information.
+   *
+   * @return The group information.
+   */
+  public GroupInfo loadedGroupInfo() {
+    return loadedGroupInfo;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.GET_GROUP_BY_ID;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdFailureEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdFailureEvent.java
new file mode 100644
index 0000000000..35838405d9
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdFailureEvent.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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+
+/** Represents an event triggered when retrieving a group by 
Gravitino-assigned id fails. */
+@DeveloperApi
+public class GetGroupByIdFailureEvent extends GroupFailureEvent {
+  private final long groupId;
+
+  /**
+   * Creates a new {@link GetGroupByIdFailureEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param exception The exception that caused the failure.
+   * @param groupId The Gravitino-assigned id of the group.
+   */
+  public GetGroupByIdFailureEvent(
+      String initiator, String metalake, Exception exception, long groupId) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, groupId), 
exception);
+    this.groupId = groupId;
+  }
+
+  /**
+   * Returns the Gravitino-assigned id of the group.
+   *
+   * @return The group id.
+   */
+  public long groupId() {
+    return groupId;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.GET_GROUP_BY_ID;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdPreEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdPreEvent.java
new file mode 100644
index 0000000000..b4c967900a
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/GetGroupByIdPreEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+
+/** Represents an event triggered before retrieving a group by 
Gravitino-assigned id. */
+@DeveloperApi
+public class GetGroupByIdPreEvent extends GroupPreEvent {
+  private final long groupId;
+
+  /**
+   * Creates a new {@link GetGroupByIdPreEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param groupId The Gravitino-assigned id of the group.
+   */
+  public GetGroupByIdPreEvent(String initiator, String metalake, long groupId) 
{
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, groupId));
+    this.groupId = groupId;
+  }
+
+  /**
+   * Returns the Gravitino-assigned id of the group being retrieved.
+   *
+   * @return The group id.
+   */
+  public long groupId() {
+    return groupId;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.GET_GROUP_BY_ID;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java 
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
index 2866195e4b..f64b9b3b0f 100644
--- 
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
@@ -144,6 +144,9 @@ public enum OperationType {
   GET_GROUP,
   GET_GROUP_BY_EXTERNAL_ID,
   REMOVE_GROUP_BY_EXTERNAL_ID,
+  GET_GROUP_BY_ID,
+  REMOVE_GROUP_BY_ID,
+  ALTER_GROUP,
   LIST_GROUPS,
   LIST_GROUP_NAMES,
   GRANT_GROUP_ROLES,
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdEvent.java
new file mode 100644
index 0000000000..cdb9e71477
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdEvent.java
@@ -0,0 +1,67 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+
+/** Represents an event triggered after removing a group by Gravitino-assigned 
id. */
+@DeveloperApi
+public class RemoveGroupByIdEvent extends GroupEvent {
+  private final long groupId;
+  private final boolean isExists;
+
+  /**
+   * Creates a new {@link RemoveGroupByIdEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param groupId The Gravitino-assigned id of the group.
+   * @param isExists Whether the group existed and was removed.
+   */
+  public RemoveGroupByIdEvent(String initiator, String metalake, long groupId, 
boolean isExists) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, groupId));
+    this.groupId = groupId;
+    this.isExists = isExists;
+  }
+
+  /**
+   * Returns the Gravitino-assigned id of the removed group.
+   *
+   * @return The group id.
+   */
+  public long groupId() {
+    return groupId;
+  }
+
+  /**
+   * Returns whether the group existed and was removed.
+   *
+   * @return {@code true} if the group was removed.
+   */
+  public boolean isExists() {
+    return isExists;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.REMOVE_GROUP_BY_ID;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdFailureEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdFailureEvent.java
new file mode 100644
index 0000000000..d3d562bc27
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdFailureEvent.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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+
+/** Represents an event triggered when removing a group by Gravitino-assigned 
id fails. */
+@DeveloperApi
+public class RemoveGroupByIdFailureEvent extends GroupFailureEvent {
+  private final long groupId;
+
+  /**
+   * Creates a new {@link RemoveGroupByIdFailureEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param exception The exception that caused the failure.
+   * @param groupId The Gravitino-assigned id of the group.
+   */
+  public RemoveGroupByIdFailureEvent(
+      String initiator, String metalake, Exception exception, long groupId) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, groupId), 
exception);
+    this.groupId = groupId;
+  }
+
+  /**
+   * Returns the Gravitino-assigned id of the group.
+   *
+   * @return The group id.
+   */
+  public long groupId() {
+    return groupId;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.REMOVE_GROUP_BY_ID;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdPreEvent.java
 
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdPreEvent.java
new file mode 100644
index 0000000000..e689a23283
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/listener/api/event/RemoveGroupByIdPreEvent.java
@@ -0,0 +1,55 @@
+/*
+ * 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.listener.api.event;
+
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.AuthorizationUtils;
+
+/** Represents an event triggered before removing a group by 
Gravitino-assigned id. */
+@DeveloperApi
+public class RemoveGroupByIdPreEvent extends GroupPreEvent {
+  private final long groupId;
+
+  /**
+   * Creates a new {@link RemoveGroupByIdPreEvent}.
+   *
+   * @param initiator The user who initiated the request.
+   * @param metalake The metalake name.
+   * @param groupId The Gravitino-assigned id of the group.
+   */
+  public RemoveGroupByIdPreEvent(String initiator, String metalake, long 
groupId) {
+    super(initiator, AuthorizationUtils.ofGroupId(metalake, groupId));
+    this.groupId = groupId;
+  }
+
+  /**
+   * Returns the Gravitino-assigned id of the group being removed.
+   *
+   * @return The group id.
+   */
+  public long groupId() {
+    return groupId;
+  }
+
+  @Override
+  public OperationType operationType() {
+    return OperationType.REMOVE_GROUP_BY_ID;
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/listener/api/info/GroupInfo.java 
b/core/src/main/java/org/apache/gravitino/listener/api/info/GroupInfo.java
index 21c0cf4414..4f5f8c65ea 100644
--- a/core/src/main/java/org/apache/gravitino/listener/api/info/GroupInfo.java
+++ b/core/src/main/java/org/apache/gravitino/listener/api/info/GroupInfo.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.listener.api.info;
 
+import com.google.common.base.Preconditions;
 import java.util.List;
 import java.util.Optional;
 import org.apache.gravitino.annotation.DeveloperApi;
@@ -27,6 +28,7 @@ import org.apache.gravitino.authorization.Group;
 /** Provides read-only access to group information for event listeners. */
 @DeveloperApi
 public class GroupInfo {
+  private final Long id;
   private final String name;
   private final Optional<String> externalId;
   private List<String> roles;
@@ -37,11 +39,21 @@ public class GroupInfo {
    * @param group the {@link Group} object from which to create the {@link 
GroupInfo}.
    */
   public GroupInfo(Group group) {
+    this.id = Preconditions.checkNotNull(group.id(), "group id");
     this.name = group.name();
     this.externalId = Optional.ofNullable(group.externalId());
     this.roles = group.roles();
   }
 
+  /**
+   * Returns the Gravitino-assigned id of the group.
+   *
+   * @return the group id
+   */
+  public Long id() {
+    return id;
+  }
+
   /**
    * Returns the name of the group.
    *
@@ -61,7 +73,7 @@ public class GroupInfo {
   }
 
   /**
-   * Returns the roles of the roles.
+   * Returns the roles of the group.
    *
    * @return The roles of the group.
    */
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java 
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
index d7e7a0e1a5..ada6e6160b 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java
@@ -353,6 +353,11 @@ public class JDBCBackend implements RelationalBackend, 
SupportsOrphanedRelationC
         return (E)
             UserMetaService.getInstance()
                 .getUserById(ident.namespace().level(0), 
Long.parseLong(ident.name()));
+      case GROUP:
+        AuthorizationUtils.checkGroupId(ident);
+        return (E)
+            GroupMetaService.getInstance()
+                .getGroupById(ident.namespace().level(0), 
Long.parseLong(ident.name()));
       default:
         throw new UnsupportedEntityTypeException(
             "Unsupported entity type: %s for get by id operation", entityType);
@@ -369,6 +374,11 @@ public class JDBCBackend implements RelationalBackend, 
SupportsOrphanedRelationC
         return (E)
             UserMetaService.getInstance()
                 .updateUserById(ident.namespace().level(0), 
Long.parseLong(ident.name()), updater);
+      case GROUP:
+        AuthorizationUtils.checkGroupId(ident);
+        return (E)
+            GroupMetaService.getInstance()
+                .updateGroupById(ident.namespace().level(0), 
Long.parseLong(ident.name()), updater);
       default:
         throw new UnsupportedEntityTypeException(
             "Unsupported entity type: %s for update by id operation", 
entityType);
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
index c86df5a90e..b74116fb24 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java
@@ -108,4 +108,10 @@ public interface GroupMetaMapper {
       method = "selectGroupMetaByMetalakeNameAndExternalId")
   GroupPO selectGroupMetaByMetalakeNameAndExternalId(
       @Param("metalakeName") String metalakeName, @Param("externalId") String 
externalId);
+
+  @SelectProvider(
+      type = GroupMetaSQLProviderFactory.class,
+      method = "selectGroupMetaByMetalakeNameAndId")
+  GroupPO selectGroupMetaByMetalakeNameAndId(
+      @Param("metalakeName") String metalakeName, @Param("groupId") Long 
groupId);
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
index 21d0d4df8e..0981d4b401 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java
@@ -115,4 +115,9 @@ public class GroupMetaSQLProviderFactory {
       @Param("metalakeName") String metalakeName, @Param("externalId") String 
externalId) {
     return 
getProvider().selectGroupMetaByMetalakeNameAndExternalId(metalakeName, 
externalId);
   }
+
+  public static String selectGroupMetaByMetalakeNameAndId(
+      @Param("metalakeName") String metalakeName, @Param("groupId") Long 
groupId) {
+    return getProvider().selectGroupMetaByMetalakeNameAndId(metalakeName, 
groupId);
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
index 9e8229ec23..fa9085a429 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java
@@ -106,6 +106,23 @@ public class GroupMetaBaseSQLProvider {
         + " AND gt.deleted_at = 0 AND mt.deleted_at = 0";
   }
 
+  public String selectGroupMetaByMetalakeNameAndId(
+      @Param("metalakeName") String metalakeName, @Param("groupId") Long 
groupId) {
+    return "SELECT gt.group_id as groupId, gt.group_name as groupName,"
+        + " gt.metalake_id as metalakeId,"
+        + " gt.external_id as externalId,"
+        + " gt.audit_info as auditInfo, gt.current_version as currentVersion,"
+        + " gt.last_version as lastVersion, gt.deleted_at as deletedAt"
+        + " FROM "
+        + GROUP_TABLE_NAME
+        + " gt JOIN "
+        + MetalakeMetaMapper.TABLE_NAME
+        + " mt ON gt.metalake_id = mt.metalake_id"
+        + " WHERE mt.metalake_name = #{metalakeName}"
+        + " AND gt.group_id = #{groupId}"
+        + " AND gt.deleted_at = 0 AND mt.deleted_at = 0";
+  }
+
   public String listExtendedGroupPOsByMetalakeIdAndNames(
       @Param("metalakeId") Long metalakeId, @Param("groupNames") List<String> 
groupNames) {
     return "<script>"
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
index 75c3a0f5c5..50b2003862 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java
@@ -393,4 +393,95 @@ public class GroupMetaService {
     return POConverters.fromGroupPO(
         groupPO, rolePOs, AuthorizationUtils.ofGroupNamespace(metalake));
   }
+
+  private GroupPO getGroupPOByMetalakeNameAndId(String metalakeName, Long 
groupId) {
+    GroupPO groupPO =
+        SessionUtils.getWithoutCommit(
+            GroupMetaMapper.class,
+            mapper -> mapper.selectGroupMetaByMetalakeNameAndId(metalakeName, 
groupId));
+
+    if (groupPO == null) {
+      throw new NoSuchEntityException(
+          NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE,
+          Entity.EntityType.GROUP.name().toLowerCase(),
+          String.valueOf(groupId));
+    }
+    return groupPO;
+  }
+
+  @Monitored(
+      metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+      baseMetricName = "getGroupById")
+  public GroupEntity getGroupById(String metalake, long groupId) {
+    GroupPO groupPO = getGroupPOByMetalakeNameAndId(metalake, groupId);
+    List<RolePO> rolePOs = 
RoleMetaService.getInstance().listRolesByGroupId(groupPO.getGroupId());
+    return POConverters.fromGroupPO(
+        groupPO, rolePOs, AuthorizationUtils.ofGroupNamespace(metalake));
+  }
+
+  @Monitored(
+      metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+      baseMetricName = "updateGroupById")
+  public <E extends Entity & HasIdentifier> GroupEntity updateGroupById(
+      String metalake, long groupId, Function<E, E> updater) throws 
IOException {
+    GroupPO oldGroupPO = getGroupPOByMetalakeNameAndId(metalake, groupId);
+    List<RolePO> rolePOs =
+        
RoleMetaService.getInstance().listRolesByGroupId(oldGroupPO.getGroupId());
+    GroupEntity oldEntity =
+        POConverters.fromGroupPO(
+            oldGroupPO, rolePOs, 
AuthorizationUtils.ofGroupNamespace(metalake));
+    GroupEntity newEntity = (GroupEntity) updater.apply((E) oldEntity);
+    Preconditions.checkArgument(
+        Objects.equals(oldEntity.id(), newEntity.id()),
+        "The updated group entity id: %s should be same with the group entity 
id before: %s",
+        newEntity.id(),
+        oldEntity.id());
+
+    try {
+      SessionUtils.doMultipleWithCommit(
+          () ->
+              SessionUtils.doWithoutCommit(
+                  GroupMetaMapper.class,
+                  mapper ->
+                      mapper.updateGroupMeta(
+                          POConverters.updateGroupPOWithVersion(oldGroupPO, 
newEntity),
+                          oldGroupPO)),
+          () ->
+              SessionUtils.doWithoutCommit(
+                  GroupMetaMapper.class,
+                  mapper -> 
mapper.touchGroupUpdatedAt(oldGroupPO.getGroupId())));
+    } catch (RuntimeException re) {
+      ExceptionUtils.checkSQLException(
+          re, Entity.EntityType.GROUP, newEntity.nameIdentifier().toString());
+      throw re;
+    }
+    return newEntity;
+  }
+
+  @Monitored(
+      metricsSource = GRAVITINO_RELATIONAL_STORE_METRIC_NAME,
+      baseMetricName = "deleteGroupById")
+  public boolean deleteGroupById(String metalake, long groupId) {
+    try {
+      getGroupPOByMetalakeNameAndId(metalake, groupId);
+    } catch (NoSuchEntityException e) {
+      return false;
+    }
+
+    SessionUtils.doMultipleWithCommit(
+        () ->
+            SessionUtils.doWithoutCommit(
+                GroupMetaMapper.class, mapper -> 
mapper.softDeleteGroupMetaByGroupId(groupId)),
+        () ->
+            SessionUtils.doWithoutCommit(
+                GroupRoleRelMapper.class,
+                mapper -> mapper.softDeleteGroupRoleRelByGroupId(groupId)),
+        () ->
+            SessionUtils.doWithoutCommit(
+                OwnerMetaMapper.class,
+                mapper ->
+                    mapper.softDeleteOwnerRelByOwnerIdAndType(
+                        groupId, Entity.EntityType.GROUP.name())));
+    return true;
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java 
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
index bf0bf7d2e4..7c6b42db62 100644
--- 
a/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
+++ 
b/core/src/test/java/org/apache/gravitino/audit/v2/TestCompatibilityUtils.java
@@ -139,6 +139,9 @@ public class TestCompatibilityUtils {
       {OperationType.REMOVE_GROUP_BY_EXTERNAL_ID, 
Operation.REMOVE_GROUP_BY_EXTERNAL_ID},
       {OperationType.GET_GROUP, Operation.GET_GROUP},
       {OperationType.GET_GROUP_BY_EXTERNAL_ID, 
Operation.GET_GROUP_BY_EXTERNAL_ID},
+      {OperationType.GET_GROUP_BY_ID, Operation.GET_GROUP_BY_ID},
+      {OperationType.REMOVE_GROUP_BY_ID, Operation.REMOVE_GROUP_BY_ID},
+      {OperationType.ALTER_GROUP, Operation.ALTER_GROUP},
       {OperationType.LIST_GROUPS, Operation.LIST_GROUPS},
       {OperationType.LIST_GROUP_NAMES, Operation.LIST_GROUP_NAMES},
       {OperationType.GRANT_GROUP_ROLES, Operation.GRANT_GROUP_ROLES},
diff --git 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
index c80b4abadc..182a184afd 100644
--- 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -659,4 +659,39 @@ public class TestAccessControlManager {
 
     Assertions.assertFalse(testProps.containsKey(StringIdentifier.ID_KEY));
   }
+
+  @Test
+  public void testGroupById() {
+    Group added = accessControlManager.addGroup(METALAKE, "id_group", 
"ext-id-group");
+    long groupId = added.id();
+    Assertions.assertNotNull(groupId);
+
+    Group loaded = accessControlManager.getGroupById(METALAKE, groupId);
+    Assertions.assertEquals(groupId, loaded.id());
+    Assertions.assertEquals("id_group", loaded.name());
+    Assertions.assertEquals("ext-id-group", loaded.externalId());
+
+    Group updated =
+        accessControlManager.alterGroupById(
+            METALAKE, groupId, GroupChange.updateExternalId("ext-id-group-2"));
+    Assertions.assertEquals(groupId, updated.id());
+    Assertions.assertEquals("ext-id-group-2", updated.externalId());
+    Assertions.assertEquals(
+        "id_group", accessControlManager.getGroupByExternalId(METALAKE, 
"ext-id-group-2").name());
+
+    Assertions.assertThrows(
+        NoSuchGroupException.class,
+        () ->
+            accessControlManager.alterGroupById(METALAKE, -1L, 
GroupChange.updateExternalId("x")));
+
+    Assertions.assertTrue(accessControlManager.removeGroupById(METALAKE, 
groupId));
+    Assertions.assertThrows(
+        NoSuchGroupException.class, () -> 
accessControlManager.getGroupById(METALAKE, groupId));
+    Assertions.assertFalse(accessControlManager.removeGroupById(METALAKE, 
groupId));
+    Assertions.assertThrows(
+        NoSuchGroupException.class,
+        () ->
+            accessControlManager.alterGroupById(
+                METALAKE, groupId, GroupChange.updateExternalId("x")));
+  }
 }
diff --git 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
index 58c869933e..ab4e0ff146 100644
--- 
a/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
+++ 
b/core/src/test/java/org/apache/gravitino/listener/api/event/TestGroupEvent.java
@@ -19,6 +19,8 @@
 
 package org.apache.gravitino.listener.api.event;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
@@ -31,6 +33,7 @@ import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AccessControlDispatcher;
 import org.apache.gravitino.authorization.AuthorizationUtils;
 import org.apache.gravitino.authorization.Group;
+import org.apache.gravitino.authorization.GroupChange;
 import org.apache.gravitino.exceptions.GravitinoRuntimeException;
 import org.apache.gravitino.exceptions.NoSuchGroupException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
@@ -51,6 +54,7 @@ public class TestGroupEvent {
   private static final String METALAKE = "demo_metalake";
   private static final String INEXIST_METALAKE = "inexist_metalake";
   private static final String GROUP_EXT_ID = "ext-group-1";
+  private static final long GROUP_ID = 1L;
   private AccessControlEventDispatcher dispatcher;
   private AccessControlEventDispatcher failureDispatcher;
   private DummyEventListener dummyEventListener;
@@ -591,8 +595,74 @@ public class TestGroupEvent {
     Assertions.assertEquals(OperationType.GET_GROUP_BY_EXTERNAL_ID, 
event.operationType());
   }
 
+  @Test
+  void testGetGroupByIdEvent() {
+    dispatcher.getGroupById(METALAKE, GROUP_ID);
+
+    PreEvent preEvent = dummyEventListener.popPreEvent();
+    Assertions.assertEquals(GetGroupByIdPreEvent.class, preEvent.getClass());
+    Assertions.assertEquals(OperationType.GET_GROUP_BY_ID, 
preEvent.operationType());
+    Assertions.assertEquals(GROUP_ID, ((GetGroupByIdPreEvent) 
preEvent).groupId());
+
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(GetGroupByIdEvent.class, event.getClass());
+    Assertions.assertEquals(OperationType.GET_GROUP_BY_ID, 
event.operationType());
+    validateGroup(((GetGroupByIdEvent) event).loadedGroupInfo(), group);
+  }
+
+  @Test
+  void testRemoveGroupByIdEvent() {
+    dispatcher.removeGroupById(METALAKE, GROUP_ID);
+
+    PreEvent preEvent = dummyEventListener.popPreEvent();
+    Assertions.assertEquals(RemoveGroupByIdPreEvent.class, 
preEvent.getClass());
+    Assertions.assertEquals(OperationType.REMOVE_GROUP_BY_ID, 
preEvent.operationType());
+
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(RemoveGroupByIdEvent.class, event.getClass());
+    Assertions.assertEquals(OperationType.REMOVE_GROUP_BY_ID, 
event.operationType());
+    Assertions.assertTrue(((RemoveGroupByIdEvent) event).isExists());
+  }
+
+  @Test
+  void testAlterGroupEvent() {
+    GroupChange change = GroupChange.updateExternalId("ext-group-2");
+    dispatcher.alterGroupById(METALAKE, GROUP_ID, change);
+
+    PreEvent preEvent = dummyEventListener.popPreEvent();
+    Assertions.assertEquals(AlterGroupPreEvent.class, preEvent.getClass());
+    Assertions.assertEquals(OperationType.ALTER_GROUP, 
preEvent.operationType());
+    AlterGroupPreEvent alterPreEvent = (AlterGroupPreEvent) preEvent;
+    Assertions.assertEquals(GROUP_ID, alterPreEvent.groupId());
+    Assertions.assertArrayEquals(new GroupChange[] {change}, 
alterPreEvent.changes());
+
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(AlterGroupEvent.class, event.getClass());
+    Assertions.assertEquals(OperationType.ALTER_GROUP, event.operationType());
+    AlterGroupEvent alterEvent = (AlterGroupEvent) event;
+    Assertions.assertArrayEquals(new GroupChange[] {change}, 
alterEvent.changes());
+    validateGroup(alterEvent.updatedGroupInfo(), group);
+  }
+
+  @Test
+  void testAlterGroupFailureEvent() {
+    GroupChange change = GroupChange.updateExternalId("ext-group-2");
+    Assertions.assertThrowsExactly(
+        GravitinoRuntimeException.class,
+        () -> failureDispatcher.alterGroupById(METALAKE, GROUP_ID, change));
+
+    dummyEventListener.popPreEvent();
+    Event event = dummyEventListener.popPostEvent();
+    Assertions.assertEquals(AlterGroupFailureEvent.class, event.getClass());
+    Assertions.assertEquals(OperationType.ALTER_GROUP, event.operationType());
+    AlterGroupFailureEvent failureEvent = (AlterGroupFailureEvent) event;
+    Assertions.assertEquals(GROUP_ID, failureEvent.groupId());
+    Assertions.assertArrayEquals(new GroupChange[] {change}, 
failureEvent.changes());
+  }
+
   private Group getMockGroup(String name, List<String> roles) {
     Group mockGroup = mock(Group.class);
+    when(mockGroup.id()).thenReturn(GROUP_ID);
     when(mockGroup.name()).thenReturn(name);
     when(mockGroup.roles()).thenReturn(roles);
 
@@ -601,6 +671,7 @@ public class TestGroupEvent {
 
   private Group getMockGroupWithExtId(String name, String externalId, 
List<String> roles) {
     Group mockGroup = mock(Group.class);
+    when(mockGroup.id()).thenReturn(GROUP_ID);
     when(mockGroup.name()).thenReturn(name);
     when(mockGroup.externalId()).thenReturn(externalId);
     when(mockGroup.roles()).thenReturn(roles);
@@ -617,12 +688,16 @@ public class TestGroupEvent {
     when(dispatcher.removeGroup(METALAKE, groupName)).thenReturn(true);
     when(dispatcher.removeGroup(METALAKE, inExistGroupName)).thenReturn(false);
     when(dispatcher.removeGroupByExternalId(METALAKE, 
GROUP_EXT_ID)).thenReturn(true);
+    when(dispatcher.removeGroupById(METALAKE, GROUP_ID)).thenReturn(true);
 
     when(dispatcher.listGroups(METALAKE)).thenReturn(new Group[] {group, 
otherGroup});
     when(dispatcher.listGroupNames(METALAKE)).thenReturn(new String[] 
{groupName, otherGroupName});
 
     when(dispatcher.getGroup(METALAKE, groupName)).thenReturn(group);
     when(dispatcher.getGroupByExternalId(METALAKE, 
GROUP_EXT_ID)).thenReturn(externalIdGroup);
+    when(dispatcher.getGroupById(METALAKE, GROUP_ID)).thenReturn(group);
+    when(dispatcher.alterGroupById(eq(METALAKE), eq(GROUP_ID), 
any(GroupChange[].class)))
+        .thenReturn(group);
     when(dispatcher.getGroup(METALAKE, inExistGroupName))
         .thenThrow(new NoSuchGroupException("group not found"));
     when(dispatcher.getGroup(INEXIST_METALAKE, groupName))
@@ -643,6 +718,7 @@ public class TestGroupEvent {
   }
 
   private void validateGroup(GroupInfo groupInfo, Group group) {
+    Assertions.assertEquals(group.id(), groupInfo.id());
     Assertions.assertEquals(group.name(), groupInfo.name());
     Assertions.assertEquals(Optional.ofNullable(group.externalId()), 
groupInfo.externalId());
     Assertions.assertEquals(group.roles(), groupInfo.roles());
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
 
b/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
index ad7acc8959..f23b8655b7 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/memory/TestMemoryEntityStore.java
@@ -240,14 +240,22 @@ public class TestMemoryEntityStore {
     public <E extends Entity & HasIdentifier> E getById(
         NameIdentifier ident, EntityType entityType, Class<E> type)
         throws NoSuchEntityException, IOException {
-      if (entityType != EntityType.USER) {
+      if (entityType != EntityType.USER && entityType != EntityType.GROUP) {
         throw new UnsupportedOperationException(
             "Get by id is not supported for entity type: " + entityType);
       }
 
-      AuthorizationUtils.checkUserId(ident);
-      long entityId = Long.parseLong(ident.name());
-      Namespace entityNamespace = 
AuthorizationUtils.ofUserNamespace(ident.namespace().level(0));
+      long entityId;
+      Namespace entityNamespace;
+      if (entityType == EntityType.USER) {
+        AuthorizationUtils.checkUserId(ident);
+        entityId = Long.parseLong(ident.name());
+        entityNamespace = 
AuthorizationUtils.ofUserNamespace(ident.namespace().level(0));
+      } else {
+        AuthorizationUtils.checkGroupId(ident);
+        entityId = Long.parseLong(ident.name());
+        entityNamespace = 
AuthorizationUtils.ofGroupNamespace(ident.namespace().level(0));
+      }
 
       for (Map.Entry<NameIdentifier, Entity> entry : entityMap.entrySet()) {
         Entity entity = entry.getValue();
@@ -284,6 +292,9 @@ public class TestMemoryEntityStore {
         if (entityType == EntityType.USER) {
           UserEntity user = getById(ident, entityType, UserEntity.class);
           return delete(user.nameIdentifier(), entityType);
+        } else if (entityType == EntityType.GROUP) {
+          GroupEntity group = getById(ident, entityType, GroupEntity.class);
+          return delete(group.nameIdentifier(), entityType);
         }
         throw new UnsupportedOperationException(
             "Delete by id is not supported for entity type: " + entityType);

Reply via email to