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

shaofengshi 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 3a21ce46c [#5061] Basic user and group CLI (#5133)
3a21ce46c is described below

commit 3a21ce46c1bca1b741db65abada371a98ee1a5e5
Author: Justin Mclean <[email protected]>
AuthorDate: Fri Nov 8 13:38:01 2024 +1100

    [#5061] Basic user and group CLI (#5133)
    
    ### What changes were proposed in this pull request?
    
    Added basic user and group commands.
    
    ### Why are the changes needed?
    
    For the CLI to support users and groups.
    
    Fix: #5061
    
    ### Does this PR introduce _any_ user-facing change?
    
    No, but extends CLI support.
    
    ### How was this patch tested?
    
    Tested locally.
---
 .../org/apache/gravitino/cli/CommandEntities.java  |  4 +
 .../org/apache/gravitino/cli/ErrorMessages.java    |  4 +
 .../apache/gravitino/cli/GravitinoCommandLine.java | 50 ++++++++++++-
 .../org/apache/gravitino/cli/GravitinoOptions.java |  9 ++-
 .../java/org/apache/gravitino/cli/Properties.java  | 20 ++---
 .../{MetalakeAuditInfo.java => CreateGroup.java}   | 37 ++++------
 .../{MetalakeAuditInfo.java => CreateUser.java}    | 37 ++++------
 .../{MetalakeAuditInfo.java => DeleteGroup.java}   | 44 +++++------
 .../{MetalakeAuditInfo.java => DeleteUser.java}    | 44 +++++------
 .../{MetalakeAuditInfo.java => GroupDetails.java}  | 41 ++++++-----
 .../{MetalakeAuditInfo.java => ListGroups.java}    | 33 ++++-----
 .../{MetalakeAuditInfo.java => ListUsers.java}     | 33 ++++-----
 .../gravitino/cli/commands/MetalakeAuditInfo.java  |  1 +
 .../{MetalakeAuditInfo.java => UserDetails.java}   | 41 ++++++-----
 .../org/apache/gravitino/cli/PropertiesTest.java   | 86 ++++++++++++++++++++--
 .../apache/gravitino/cli/TestCommandEntities.java  |  2 -
 docs/cli.md                                        | 76 +++++++++++++++++--
 17 files changed, 370 insertions(+), 192 deletions(-)

diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java
index 66e679304..a7703b157 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/CommandEntities.java
@@ -31,6 +31,8 @@ public class CommandEntities {
   public static final String SCHEMA = "schema";
   public static final String TABLE = "table";
   public static final String COLUMN = "column";
+  public static final String USER = "user";
+  public static final String GROUP = "group";
 
   private static final HashSet<String> VALID_ENTITIES = new HashSet<>();
 
@@ -40,6 +42,8 @@ public class CommandEntities {
     VALID_ENTITIES.add(SCHEMA);
     VALID_ENTITIES.add(TABLE);
     VALID_ENTITIES.add(COLUMN);
+    VALID_ENTITIES.add(USER);
+    VALID_ENTITIES.add(GROUP);
   }
 
   /**
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
index 0e7816ae4..c3da72ce8 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/ErrorMessages.java
@@ -33,4 +33,8 @@ public class ErrorMessages {
   public static final String METALAKE_EXISTS = "Metalake already exists.";
   public static final String CATALOG_EXISTS = "Catalog already exists.";
   public static final String SCHEMA_EXISTS = "Schema already exists.";
+  public static final String UNKNOWN_USER = "Unknown user.";
+  public static final String USER_EXISTS = "User already exists.";
+  public static final String UNKNOWN_GROUP = "Unknown group.";
+  public static final String GROUP_EXISTS = "Group already exists.";
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
index 356956387..566ff88e9 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoCommandLine.java
@@ -26,20 +26,27 @@ import org.apache.commons.cli.Options;
 import org.apache.gravitino.cli.commands.CatalogDetails;
 import org.apache.gravitino.cli.commands.ClientVersion;
 import org.apache.gravitino.cli.commands.CreateCatalog;
+import org.apache.gravitino.cli.commands.CreateGroup;
 import org.apache.gravitino.cli.commands.CreateMetalake;
 import org.apache.gravitino.cli.commands.CreateSchema;
+import org.apache.gravitino.cli.commands.CreateUser;
 import org.apache.gravitino.cli.commands.DeleteCatalog;
+import org.apache.gravitino.cli.commands.DeleteGroup;
 import org.apache.gravitino.cli.commands.DeleteMetalake;
 import org.apache.gravitino.cli.commands.DeleteSchema;
 import org.apache.gravitino.cli.commands.DeleteTable;
+import org.apache.gravitino.cli.commands.DeleteUser;
+import org.apache.gravitino.cli.commands.GroupDetails;
 import org.apache.gravitino.cli.commands.ListCatalogProperties;
 import org.apache.gravitino.cli.commands.ListCatalogs;
 import org.apache.gravitino.cli.commands.ListColumns;
+import org.apache.gravitino.cli.commands.ListGroups;
 import org.apache.gravitino.cli.commands.ListMetalakeProperties;
 import org.apache.gravitino.cli.commands.ListMetalakes;
 import org.apache.gravitino.cli.commands.ListSchema;
 import org.apache.gravitino.cli.commands.ListSchemaProperties;
 import org.apache.gravitino.cli.commands.ListTables;
+import org.apache.gravitino.cli.commands.ListUsers;
 import org.apache.gravitino.cli.commands.MetalakeAuditInfo;
 import org.apache.gravitino.cli.commands.MetalakeDetails;
 import org.apache.gravitino.cli.commands.RemoveCatalogProperty;
@@ -55,6 +62,7 @@ import org.apache.gravitino.cli.commands.UpdateCatalogComment;
 import org.apache.gravitino.cli.commands.UpdateCatalogName;
 import org.apache.gravitino.cli.commands.UpdateMetalakeComment;
 import org.apache.gravitino.cli.commands.UpdateMetalakeName;
+import org.apache.gravitino.cli.commands.UserDetails;
 
 /* Gravitino Command line */
 public class GravitinoCommandLine {
@@ -152,6 +160,10 @@ public class GravitinoCommandLine {
       handleCatalogCommand();
     } else if (entity.equals(CommandEntities.METALAKE)) {
       handleMetalakeCommand();
+    } else if (entity.equals(CommandEntities.USER)) {
+      handleUserCommand();
+    } else if (entity.equals(CommandEntities.GROUP)) {
+      handleGroupCommand();
     }
   }
 
@@ -217,7 +229,7 @@ public class GravitinoCommandLine {
     } else if (CommandActions.CREATE.equals(command)) {
       String comment = line.getOptionValue(GravitinoOptions.COMMENT);
       String provider = line.getOptionValue(GravitinoOptions.PROVIDER);
-      String properties = line.getOptionValue(GravitinoOptions.PROPERTIES);
+      String[] properties = line.getOptionValues(GravitinoOptions.PROPERTIES);
       Map<String, String> propertyMap = new Properties().parse(properties);
       new CreateCatalog(url, ignore, metalake, catalog, provider, comment, 
propertyMap).handle();
     } else if (CommandActions.DELETE.equals(command)) {
@@ -304,6 +316,42 @@ public class GravitinoCommandLine {
     }
   }
 
+  /** Handles the command execution for Users based on command type and the 
command line options. */
+  protected void handleUserCommand() {
+    String url = getUrl();
+    FullName name = new FullName(line);
+    String metalake = name.getMetalakeName();
+    String user = line.getOptionValue(GravitinoOptions.USER);
+
+    if (CommandActions.DETAILS.equals(command)) {
+      new UserDetails(url, ignore, metalake, user).handle();
+    } else if (CommandActions.LIST.equals(command)) {
+      new ListUsers(url, ignore, metalake).handle();
+    } else if (CommandActions.CREATE.equals(command)) {
+      new CreateUser(url, ignore, metalake, user).handle();
+    } else if (CommandActions.DELETE.equals(command)) {
+      new DeleteUser(url, ignore, metalake, user).handle();
+    }
+  }
+
+  /** Handles the command execution for Group based on command type and the 
command line options. */
+  protected void handleGroupCommand() {
+    String url = getUrl();
+    FullName name = new FullName(line);
+    String metalake = name.getMetalakeName();
+    String group = line.getOptionValue(GravitinoOptions.GROUP);
+
+    if (CommandActions.DETAILS.equals(command)) {
+      new GroupDetails(url, ignore, metalake, group).handle();
+    } else if (CommandActions.LIST.equals(command)) {
+      new ListGroups(url, ignore, metalake).handle();
+    } else if (CommandActions.CREATE.equals(command)) {
+      new CreateGroup(url, ignore, metalake, group).handle();
+    } else if (CommandActions.DELETE.equals(command)) {
+      new DeleteGroup(url, ignore, metalake, group).handle();
+    }
+  }
+
   /**
    * Handles the command execution for Columns based on command type and the 
command line options.
    */
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
index 53b9fa2eb..d9ccbb4ad 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/GravitinoOptions.java
@@ -37,6 +37,8 @@ public class GravitinoOptions {
   public static final String VALUE = "value";
   public static final String PROVIDER = "provider";
   public static final String PROPERTIES = "properties";
+  public static final String USER = "user";
+  public static final String GROUP = "group";
   public static final String AUDIT = "audit";
 
   /**
@@ -64,12 +66,13 @@ public class GravitinoOptions {
     options.addOption(createArgOption("V", VALUE, "property value"));
     options.addOption(
         createArgOption(
-            "g", PROVIDER, "provider one of hadoop, hive, mysql, postgres, 
iceberg, kafka"));
+            "t", PROVIDER, "provider one of hadoop, hive, mysql, postgres, 
iceberg, kafka"));
+    options.addOption(createArgOption("l", USER, "user name"));
+    options.addOption(createArgOption("g", GROUP, "group name"));
 
     // Properties option can have multiple values
     Option properties =
-        createArgOption("p", PROPERTIES, "comma separated property name/value 
pairs");
-    properties.hasArgs();
+        Option.builder("p").longOpt(PROPERTIES).desc("property name/value 
pairs").hasArgs().build();
     options.addOption(properties);
 
     return options;
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/Properties.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/Properties.java
index f9985f235..1c166102f 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/Properties.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/Properties.java
@@ -55,19 +55,21 @@ public class Properties {
    * <p>Each pair in the input string is split by the specified delimiter, and 
then each pair is
    * further split by the key-value separator.
    *
-   * @param input The input string containing name-value pairs.
+   * @param inputs An arrays of input strings containing name-value pairs.
    * @return A map of entries, where each entry represents a key-value pair 
from the input string.
    */
-  public Map<String, String> parse(String input) {
+  public Map<String, String> parse(String[] inputs) {
     HashMap<String, String> map = new HashMap<>();
 
-    // Split the input by the delimiter into key-value pairs
-    String[] pairs = input.split(delimiter);
-    for (String pair : pairs) {
-      // Split each key-value pair by the separator
-      String[] keyValue = pair.split(keyValueSeparator, 2);
-      if (keyValue.length == 2) {
-        map.put(keyValue[0].trim(), keyValue[1].trim());
+    for (String input : inputs) {
+      // Split the input by the delimiter into key-value pairs
+      String[] pairs = input.split(delimiter);
+      for (String pair : pairs) {
+        // Split each key-value pair by the separator
+        String[] keyValue = pair.split(keyValueSeparator, 2);
+        if (keyValue.length == 2) {
+          map.put(keyValue[0].trim(), keyValue[1].trim());
+        }
       }
     }
 
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateGroup.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateGroup.java
index 6c1d49a20..8f9fa985d 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateGroup.java
@@ -19,51 +19,46 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
+public class CreateGroup extends Command {
   protected final String metalake;
+  protected final String group;
 
   /**
-   * Displays metalake audit information.
+   * Create a new group.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
+   * @param group The name of the group.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public CreateGroup(String url, boolean ignoreVersions, String metalake, 
String group) {
     super(url, ignoreVersions);
     this.metalake = metalake;
+    this.group = group;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Create a new group. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    try {
+      GravitinoClient client = buildClient(metalake);
+      client.addGroup(group);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
+    } catch (GroupAlreadyExistsException err) {
+      System.err.println(ErrorMessages.GROUP_EXISTS);
+      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
-
-    System.out.println(auditInfo);
+    System.out.println(group + " created");
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateUser.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateUser.java
index 6c1d49a20..150bf64ce 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/CreateUser.java
@@ -19,51 +19,46 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.UserAlreadyExistsException;
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
+public class CreateUser extends Command {
   protected final String metalake;
+  protected final String user;
 
   /**
-   * Displays metalake audit information.
+   * Create a new User.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
+   * @param user The name of the user.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public CreateUser(String url, boolean ignoreVersions, String metalake, 
String user) {
     super(url, ignoreVersions);
     this.metalake = metalake;
+    this.user = user;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Create a new user. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    try {
+      GravitinoClient client = buildClient(metalake);
+      client.addUser(user);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
+    } catch (UserAlreadyExistsException err) {
+      System.err.println(ErrorMessages.USER_EXISTS);
+      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
-
-    System.out.println(auditInfo);
+    System.out.println(user + " created");
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteGroup.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteGroup.java
index 6c1d49a20..e13713752 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteGroup.java
@@ -19,51 +19,53 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.exceptions.NoSuchGroupException;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
+public class DeleteGroup extends Command {
+
   protected final String metalake;
+  protected final String group;
 
   /**
-   * Displays metalake audit information.
+   * Delete a group.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
+   * @param group The name of the group.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public DeleteGroup(String url, boolean ignoreVersions, String metalake, 
String group) {
     super(url, ignoreVersions);
     this.metalake = metalake;
+    this.group = group;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Delete a group. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    boolean deleted = false;
+
+    try {
+      GravitinoClient client = buildClient(metalake);
+      deleted = client.removeGroup(group);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
+    } catch (NoSuchGroupException err) {
+      System.err.println(ErrorMessages.UNKNOWN_GROUP);
+      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
-
-    System.out.println(auditInfo);
+    if (deleted) {
+      System.out.println(group + " deleted.");
+    } else {
+      System.out.println(group + " not deleted.");
+    }
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteUser.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteUser.java
index 6c1d49a20..ae43866a1 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/DeleteUser.java
@@ -19,51 +19,53 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchUserException;
+
+public class DeleteUser extends Command {
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
   protected final String metalake;
+  protected final String user;
 
   /**
-   * Displays metalake audit information.
+   * Deletes a user.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
+   * @param user The name of the user.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public DeleteUser(String url, boolean ignoreVersions, String metalake, 
String user) {
     super(url, ignoreVersions);
     this.metalake = metalake;
+    this.user = user;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Delete a user. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    boolean deleted = false;
+
+    try {
+      GravitinoClient client = buildClient(metalake);
+      deleted = client.removeUser(user);
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
+    } catch (NoSuchUserException err) {
+      System.err.println(ErrorMessages.UNKNOWN_USER);
+      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
-
-    System.out.println(auditInfo);
+    if (deleted) {
+      System.out.println(user + " deleted.");
+    } else {
+      System.out.println(user + " not deleted.");
+    }
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GroupDetails.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/GroupDetails.java
index 6c1d49a20..d21806ef2 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GroupDetails.java
@@ -19,51 +19,52 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
+import java.util.List;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchUserException;
+
+public class GroupDetails extends Command {
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
   protected final String metalake;
+  protected final String group;
 
   /**
-   * Displays metalake audit information.
+   * Displays the roles in a group.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
+   * @param group The name of the group.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public GroupDetails(String url, boolean ignoreVersions, String metalake, 
String group) {
     super(url, ignoreVersions);
     this.metalake = metalake;
+    this.group = group;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Displays the roles of a specified group. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    List<String> roles = null;
+
+    try {
+      GravitinoClient client = buildClient(metalake);
+      roles = client.getGroup(group).roles();
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
+    } catch (NoSuchUserException err) {
+      System.err.println(ErrorMessages.UNKNOWN_GROUP);
+      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
+    String all = String.join(",", roles);
 
-    System.out.println(auditInfo);
+    System.out.println(all.toString());
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
index 6c1d49a20..d529b5147 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListGroups.java
@@ -19,32 +19,34 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
+/* Lists all groups in a metalake. */
+public class ListGroups extends Command {
+
   protected final String metalake;
 
   /**
-   * Displays metalake audit information.
+   * Lists all groups in a metalake.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public ListGroups(String url, boolean ignoreVersions, String metalake) {
     super(url, ignoreVersions);
     this.metalake = metalake;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Lists all groups in a metalake. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    String[] groups = new String[0];
+    try {
+      GravitinoClient client = buildClient(metalake);
+      groups = client.listGroupNames();
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
@@ -53,17 +55,8 @@ public class MetalakeAuditInfo extends Command {
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
+    String all = String.join(",", groups);
 
-    System.out.println(auditInfo);
+    System.out.println(all.toString());
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
index 6c1d49a20..465075a97 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/ListUsers.java
@@ -19,32 +19,34 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
+/* Lists all users in a metalake. */
+public class ListUsers extends Command {
+
   protected final String metalake;
 
   /**
-   * Displays metalake audit information.
+   * Lists all users in a metalake.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public ListUsers(String url, boolean ignoreVersions, String metalake) {
     super(url, ignoreVersions);
     this.metalake = metalake;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Lists all users in a metalake. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    String[] users = new String[0];
+    try {
+      GravitinoClient client = buildClient(metalake);
+      users = client.listUserNames();
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
@@ -53,17 +55,8 @@ public class MetalakeAuditInfo extends Command {
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
+    String all = String.join(",", users);
 
-    System.out.println(auditInfo);
+    System.out.println(all.toString());
   }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
index 6c1d49a20..de3807839 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
@@ -41,6 +41,7 @@ public class MetalakeAuditInfo extends Command {
   }
 
   /** Displays the audit information of a metalake. */
+  @Override
   public void handle() {
     Audit audit;
     try (GravitinoClient client = buildClient(metalake)) {
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
 b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UserDetails.java
similarity index 66%
copy from 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
copy to 
clients/cli/src/main/java/org/apache/gravitino/cli/commands/UserDetails.java
index 6c1d49a20..43ebb65e8 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetalakeAuditInfo.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/UserDetails.java
@@ -19,51 +19,52 @@
 
 package org.apache.gravitino.cli.commands;
 
-import org.apache.gravitino.Audit;
+import java.util.List;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchUserException;
+
+public class UserDetails extends Command {
 
-/** Displays the audit information of a metalake. */
-public class MetalakeAuditInfo extends Command {
   protected final String metalake;
+  protected final String user;
 
   /**
-   * Displays metalake audit information.
+   * Displays the roles of a user.
    *
    * @param url The URL of the Gravitino server.
    * @param ignoreVersions If true don't check the client/server versions 
match.
    * @param metalake The name of the metalake.
+   * @param user The name of the user.
    */
-  public MetalakeAuditInfo(String url, boolean ignoreVersions, String 
metalake) {
+  public UserDetails(String url, boolean ignoreVersions, String metalake, 
String user) {
     super(url, ignoreVersions);
     this.metalake = metalake;
+    this.user = user;
   }
 
-  /** Displays the audit information of a metalake. */
+  /** Displays the roles of a specified user. */
+  @Override
   public void handle() {
-    Audit audit;
-    try (GravitinoClient client = buildClient(metalake)) {
-      audit = client.loadMetalake(metalake).auditInfo();
+    List<String> roles = null;
+
+    try {
+      GravitinoClient client = buildClient(metalake);
+      roles = client.getUser(user).roles();
     } catch (NoSuchMetalakeException err) {
       System.err.println(ErrorMessages.UNKNOWN_METALAKE);
       return;
+    } catch (NoSuchUserException err) {
+      System.err.println(ErrorMessages.UNKNOWN_USER);
+      return;
     } catch (Exception exp) {
       System.err.println(exp.getMessage());
       return;
     }
 
-    String auditInfo =
-        "creator,createTime,lastModifier,lastModifiedTime"
-            + System.lineSeparator()
-            + audit.creator()
-            + ","
-            + audit.createTime()
-            + ","
-            + audit.lastModifier()
-            + ","
-            + audit.lastModifiedTime();
+    String all = String.join(",", roles);
 
-    System.out.println(auditInfo);
+    System.out.println(all.toString());
   }
 }
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/PropertiesTest.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/PropertiesTest.java
index cdf08a59d..17af8c21a 100644
--- a/clients/cli/src/test/java/org/apache/gravitino/cli/PropertiesTest.java
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/PropertiesTest.java
@@ -30,7 +30,20 @@ public class PropertiesTest {
   @Test
   public void testDefaultDelimiterAndSeparator() {
     Properties properties = new Properties();
-    String input = "key1=value1,key2=value2,key3=value3";
+    String[] input = {"key1=value1,key2=value2,key3=value3"};
+
+    Map<String, String> result = properties.parse(input);
+
+    assertEquals(3, result.size());
+    assertEquals("value1", result.get("key1"));
+    assertEquals("value2", result.get("key2"));
+    assertEquals("value3", result.get("key3"));
+  }
+
+  @Test
+  public void testDefaultDelimiterAndSeparatorArray() {
+    Properties properties = new Properties();
+    String[] input = {"key1=value1", "key2=value2", "key3=value3"};
 
     Map<String, String> result = properties.parse(input);
 
@@ -43,7 +56,20 @@ public class PropertiesTest {
   @Test
   public void testCustomDelimiterAndSeparator() {
     Properties properties = new Properties(";", ":");
-    String input = "key1:value1;key2:value2;key3:value3";
+    String[] input = {"key1:value1;key2:value2;key3:value3"};
+
+    Map<String, String> result = properties.parse(input);
+
+    assertEquals(3, result.size());
+    assertEquals("value1", result.get("key1"));
+    assertEquals("value2", result.get("key2"));
+    assertEquals("value3", result.get("key3"));
+  }
+
+  @Test
+  public void testCustomDelimiterAndSeparatorArray() {
+    Properties properties = new Properties(";", ":");
+    String[] input = {"key1:value1;key2:value2", "key3:value3"};
 
     Map<String, String> result = properties.parse(input);
 
@@ -56,17 +82,27 @@ public class PropertiesTest {
   @Test
   public void testEmptyInput() {
     Properties properties = new Properties();
-    String input = "";
+    String input[] = {""};
 
     Map<String, String> result = properties.parse(input);
 
     assertTrue(result.isEmpty(), "Result should be empty for empty input");
   }
 
+  @Test
+  public void testEmptyArray() {
+    Properties properties = new Properties();
+    String input[] = {};
+
+    Map<String, String> result = properties.parse(input);
+
+    assertTrue(result.isEmpty(), "Result should be empty for empty array");
+  }
+
   @Test
   public void testSinglePair() {
     Properties properties = new Properties();
-    String input = "key1=value1";
+    String[] input = {"key1=value1"};
 
     Map<String, String> result = properties.parse(input);
 
@@ -77,7 +113,19 @@ public class PropertiesTest {
   @Test
   public void testMalformedPair() {
     Properties properties = new Properties();
-    String input = "key1=value1,key2,key3=value3";
+    String[] input = {"key1=value1,key2,key3=value3"};
+
+    Map<String, String> result = properties.parse(input);
+
+    assertEquals(2, result.size());
+    assertEquals("value1", result.get("key1"));
+    assertEquals("value3", result.get("key3"));
+  }
+
+  @Test
+  public void testMalformedPairArray() {
+    Properties properties = new Properties();
+    String[] input = {"key1=value1", "key2", "key3=value3"};
 
     Map<String, String> result = properties.parse(input);
 
@@ -89,7 +137,19 @@ public class PropertiesTest {
   @Test
   public void testWhitespaceHandling() {
     Properties properties = new Properties();
-    String input = " key1 = value1 , key2 = value2 ";
+    String[] input = {" key1 = value1 , key2 = value2 "};
+
+    Map<String, String> result = properties.parse(input);
+
+    assertEquals(2, result.size());
+    assertEquals("value1", result.get("key1"));
+    assertEquals("value2", result.get("key2"));
+  }
+
+  @Test
+  public void testWhitespaceHandlingArray() {
+    Properties properties = new Properties();
+    String[] input = {" key1 = value1 ", " key2 = value2 "};
 
     Map<String, String> result = properties.parse(input);
 
@@ -101,7 +161,19 @@ public class PropertiesTest {
   @Test
   public void testDuplicateKeys() {
     Properties properties = new Properties();
-    String input = "key1=value1,key1=value2,key2=value3";
+    String[] input = {"key1=value1,key1=value2,key2=value3"};
+
+    Map<String, String> result = properties.parse(input);
+
+    assertEquals(2, result.size());
+    assertEquals("value2", result.get("key1"), "Last value should overwrite 
previous ones");
+    assertEquals("value3", result.get("key2"));
+  }
+
+  @Test
+  public void testDuplicateKeysArray() {
+    Properties properties = new Properties();
+    String[] input = {"key1=value1", "key1=value2", "key2=value3"};
 
     Map<String, String> result = properties.parse(input);
 
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestCommandEntities.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCommandEntities.java
index 49ee9770b..2ca43d060 100644
--- 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestCommandEntities.java
+++ 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestCommandEntities.java
@@ -37,8 +37,6 @@ public class TestCommandEntities {
         CommandEntities.isValidEntity(CommandEntities.SCHEMA), "SCHEMA should 
be a valid entity");
     assertTrue(
         CommandEntities.isValidEntity(CommandEntities.TABLE), "TABLE should be 
a valid entity");
-    assertTrue(
-        CommandEntities.isValidEntity(CommandEntities.COLUMN), "COLUMN should 
be a valid entity");
   }
 
   @Test
diff --git a/docs/cli.md b/docs/cli.md
index 589129815..d54ba2c33 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -30,16 +30,18 @@ The general structure for running commands with the 
Gravitino CLI is `gcli entit
  usage: gcli [metalake|catalog|schema|table|column] 
[list|details|create|delete|update|set|remove|properties] [options]
  Options
  -c,--comment <arg>      entity comment
- -g,--provider <arg>     provider one of hadoop, hive, mysql, postgres,
-                         iceberg, kafka
+ -g,--group <arg>        group name
  -h,--help               command help information
  -i,--ignore             Ignore client/sever version check
+ -l,--user <arg>         user name
  -m,--metalake <arg>     Metalake name
  -n,--name <arg>         full entity name (dot separated)
  -P,--property <arg>     property name
- -p,--properties <arg>   comma separated property name/value pairs
+ -p,--properties <arg>   property name/value pairs
  -r,--rename <arg>       new entity name
  -s,--server             Gravitino server version
+ -t,--provider <arg>     provider one of hadoop, hive, mysql, postgres,
+                         iceberg, kafka
  -u,--url <arg>          Gravitino URL (default: http://localhost:8090)
  -v,--version            Gravitino client version
  -V,--value <arg>        property value
@@ -134,6 +136,16 @@ If the client and server are running different versions of 
the Gravitino softwar
 2. Set via the `GRAVITINO_IGNORE` environment variable.
 3. Stored in the Gravitino CLI configuration file.
 
+### Multiple properties
+
+For commands that accept multiple properties they can be specified in a couple 
of different ways:
+
+1. gcli --properties n1=v1,n2=v2,n3=v3
+
+2. gcli --properties n1=v1 n2=v2 n3=v3
+
+3. gcli --properties n1=v1 --properties n2=v2 --properties n3=v3
+
 ### Metalake commands
 
 #### Show all metalakes
@@ -190,7 +202,7 @@ gcli metalake set --metalake metalake_demo --property test 
--value value
 gcli metalake remove --metalake metalake_demo --property test
 ```
 
-### Catalog
+### Catalog commands
 
 #### Show all catalogs in a metalake
 
@@ -274,7 +286,7 @@ gcli catalog set --metalake metalake_demo --name 
catalog_mysql --property test -
 gcli catalog remove --metalake metalake_demo --name catalog_mysql --property 
test
 ```
 
-### Schema
+### Schema commands
 
 #### Show all schemas in a catalog
 
@@ -302,7 +314,7 @@ gcli schema properties --metalake metalake_demo --name 
catalog_postgres.hr -i
 
 Setting and removing schema properties is not currently supported by the Java 
API or the Gravitino CLI.
 
-### Table
+### Table commands
 
 #### Show all tables
 
@@ -321,3 +333,55 @@ gcli column list --metalake metalake_demo --name 
catalog_postgres.hr.departments
 ```bash
 gcli table delete --metalake metalake_demo --name catalog_postgres.hr.salaries
 ```
+
+### User commands
+
+#### Create a user
+
+```bash
+gcli user create --metalake metalake_demo --user new_user
+```
+
+#### Show a user's details
+
+```bash
+gcli user details --metalake metalake_demo --user new_user
+```
+
+#### List all users
+
+```bash
+gcli user list --metalake metalake_demo
+```
+
+#### Delete a user
+
+```bash
+gcli user delete --metalake metalake_demo --user new_user
+```
+
+### Group commands
+
+#### Create a group
+
+```bash
+gcli group create --metalake metalake_demo --group new_group
+```
+
+#### Display a group's details
+
+```bash
+gcli group details --metalake metalake_demo --group new_group
+```
+
+#### List all groups
+
+```bash
+gcli group list --metalake metalake_demo
+```
+
+#### Delete a group
+
+```bash
+gcli group delete --metalake metalake_demo --group new_group
+```


Reply via email to