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 7ad9377d0 [#5162] Add grant and revoke privileges to the Gravitino 
CLI. (#5783)
7ad9377d0 is described below

commit 7ad9377d0beeb06c92149c7295970766a826ca57
Author: Justin Mclean <[email protected]>
AuthorDate: Thu Dec 19 23:32:50 2024 +1100

    [#5162] Add grant and revoke privileges to the Gravitino CLI. (#5783)
    
    ### What changes were proposed in this pull request?
    
    Add grant and revoke privileges to the Gravitino CLI.
    
    ### Why are the changes needed?
    
    To complete the role commands.
    
    Fix: #5162
    
    ### Does this PR introduce _any_ user-facing change?
    
    No but it adds two more commands.
    
    ### How was this patch tested?
    
    Tested locally.
---
 .../java/org/apache/gravitino/cli/FullName.java    |  13 +++
 .../apache/gravitino/cli/GravitinoCommandLine.java |   9 ++
 .../org/apache/gravitino/cli/GravitinoOptions.java |   2 +
 .../java/org/apache/gravitino/cli/Privileges.java  | 119 +++++++++++++++++++++
 .../apache/gravitino/cli/TestableCommandLine.java  |  22 ++++
 .../cli/commands/GrantPrivilegesToRole.java        | 106 ++++++++++++++++++
 .../gravitino/cli/commands/MetadataCommand.java    |  83 ++++++++++++++
 .../cli/commands/RevokePrivilegesFromRole.java     | 106 ++++++++++++++++++
 .../apache/gravitino/cli/commands/RoleDetails.java |  13 ++-
 .../org/apache/gravitino/cli/TestPrivileges.java   |  52 +++++++++
 .../org/apache/gravitino/cli/TestRoleCommands.java |  62 +++++++++++
 docs/cli.md                                        |  19 ++++
 12 files changed, 601 insertions(+), 5 deletions(-)

diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java
index c53a5adc8..46a3bb92d 100644
--- a/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/FullName.java
@@ -133,6 +133,19 @@ public class FullName {
     return getNamePart(3);
   }
 
+  /**
+   * Retrieves the name from the command line options.
+   *
+   * @return The name, or null if not found.
+   */
+  public String getName() {
+    if (line.hasOption(GravitinoOptions.NAME)) {
+      return line.getOptionValue(GravitinoOptions.NAME);
+    }
+
+    return null;
+  }
+
   /**
    * Helper method to retrieve a specific part of the full name based on the 
position of the part.
    *
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 3603a230f..f0e65dd86 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
@@ -672,6 +672,7 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
     FullName name = new FullName(line);
     String metalake = name.getMetalakeName();
     String role = line.getOptionValue(GravitinoOptions.ROLE);
+    String[] privileges = line.getOptionValues(GravitinoOptions.PRIVILEGE);
 
     Command.setAuthenticationMode(auth, userName);
 
@@ -697,6 +698,14 @@ public class GravitinoCommandLine extends 
TestableCommandLine {
         newDeleteRole(url, ignore, forceDelete, metalake, role).handle();
         break;
 
+      case CommandActions.GRANT:
+        newGrantPrivilegesToRole(url, ignore, metalake, role, name, 
privileges).handle();
+        break;
+
+      case CommandActions.REVOKE:
+        newRevokePrivilegesFromRole(url, ignore, metalake, role, name, 
privileges).handle();
+        break;
+
       default:
         System.err.println(ErrorMessages.UNSUPPORTED_ACTION);
         break;
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 91993226b..a42591026 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
@@ -45,6 +45,7 @@ public class GravitinoOptions {
   public static final String OWNER = "owner";
   public static final String PARTITION = "partition";
   public static final String POSITION = "position";
+  public static final String PRIVILEGE = "privilege";
   public static final String PROPERTIES = "properties";
   public static final String PROPERTY = "property";
   public static final String PROVIDER = "provider";
@@ -105,6 +106,7 @@ public class GravitinoOptions {
     // Options that support multiple values
     options.addOption(createArgsOption("p", PROPERTIES, "property name/value 
pairs"));
     options.addOption(createArgsOption("t", TAG, "tag name"));
+    options.addOption(createArgsOption(null, PRIVILEGE, "privilege(s)"));
     options.addOption(createArgsOption("r", ROLE, "role name"));
 
     // Force delete entities and rename metalake operations
diff --git a/clients/cli/src/main/java/org/apache/gravitino/cli/Privileges.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/Privileges.java
new file mode 100644
index 000000000..9d47d8fc9
--- /dev/null
+++ b/clients/cli/src/main/java/org/apache/gravitino/cli/Privileges.java
@@ -0,0 +1,119 @@
+/*
+ * 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.cli;
+
+import java.util.HashSet;
+import org.apache.gravitino.authorization.Privilege;
+
+public class Privileges {
+  public static final String CREATE_CATALOG = "create_catalog";
+  public static final String USE_CATALOG = "use_catalog";
+  public static final String CREATE_SCHEMA = "create_schema";
+  public static final String USE_SCHEMA = "use_schema";
+  public static final String CREATE_TABLE = "create_table";
+  public static final String MODIFY_TABLE = "modify_table";
+  public static final String SELECT_TABLE = "select_table";
+  public static final String CREATE_FILESET = "create_fileset";
+  public static final String WRITE_FILESET = "write_fileset";
+  public static final String READ_FILESET = "read_fileset";
+  public static final String CREATE_TOPIC = "create_topic";
+  public static final String PRODUCE_TOPIC = "produce_topic";
+  public static final String CONSUME_TOPIC = "consume_topic";
+  public static final String MANAGE_USERS = "manage_users";
+  public static final String CREATE_ROLE = "create_role";
+  public static final String MANAGE_GRANTS = "manage_grants";
+
+  private static final HashSet<String> VALID_PRIVILEGES = new HashSet<>();
+
+  static {
+    VALID_PRIVILEGES.add(CREATE_CATALOG);
+    VALID_PRIVILEGES.add(USE_CATALOG);
+    VALID_PRIVILEGES.add(CREATE_SCHEMA);
+    VALID_PRIVILEGES.add(USE_SCHEMA);
+    VALID_PRIVILEGES.add(CREATE_TABLE);
+    VALID_PRIVILEGES.add(MODIFY_TABLE);
+    VALID_PRIVILEGES.add(SELECT_TABLE);
+    VALID_PRIVILEGES.add(CREATE_FILESET);
+    VALID_PRIVILEGES.add(WRITE_FILESET);
+    VALID_PRIVILEGES.add(READ_FILESET);
+    VALID_PRIVILEGES.add(CREATE_TOPIC);
+    VALID_PRIVILEGES.add(PRODUCE_TOPIC);
+    VALID_PRIVILEGES.add(CONSUME_TOPIC);
+    VALID_PRIVILEGES.add(MANAGE_USERS);
+    VALID_PRIVILEGES.add(CREATE_ROLE);
+    VALID_PRIVILEGES.add(MANAGE_GRANTS);
+  }
+
+  /**
+   * Checks if a given privilege is a valid one.
+   *
+   * @param privilege The privilege to check.
+   * @return true if the privilege is valid, false otherwise.
+   */
+  public static boolean isValid(String privilege) {
+    return VALID_PRIVILEGES.contains(privilege);
+  }
+
+  /**
+   * Converts a string representation of a privilege to the corresponding 
{@link Privilege.Name}.
+   *
+   * @param privilege the privilege to be converted.
+   * @return the corresponding {@link Privilege.Name} constant, or nullif the 
privilege is unknown.
+   */
+  public static Privilege.Name toName(String privilege) {
+    switch (privilege) {
+      case CREATE_CATALOG:
+        return Privilege.Name.CREATE_CATALOG;
+      case USE_CATALOG:
+        return Privilege.Name.USE_CATALOG;
+      case CREATE_SCHEMA:
+        return Privilege.Name.CREATE_SCHEMA;
+      case USE_SCHEMA:
+        return Privilege.Name.USE_SCHEMA;
+      case CREATE_TABLE:
+        return Privilege.Name.CREATE_TABLE;
+      case MODIFY_TABLE:
+        return Privilege.Name.MODIFY_TABLE;
+      case SELECT_TABLE:
+        return Privilege.Name.SELECT_TABLE;
+      case CREATE_FILESET:
+        return Privilege.Name.CREATE_FILESET;
+      case WRITE_FILESET:
+        return Privilege.Name.WRITE_FILESET;
+      case READ_FILESET:
+        return Privilege.Name.READ_FILESET;
+      case CREATE_TOPIC:
+        return Privilege.Name.CREATE_TOPIC;
+      case PRODUCE_TOPIC:
+        return Privilege.Name.PRODUCE_TOPIC;
+      case CONSUME_TOPIC:
+        return Privilege.Name.CONSUME_TOPIC;
+      case MANAGE_USERS:
+        return Privilege.Name.MANAGE_USERS;
+      case CREATE_ROLE:
+        return Privilege.Name.CREATE_ROLE;
+      case MANAGE_GRANTS:
+        return Privilege.Name.MANAGE_GRANTS;
+      default:
+        System.err.println("Unknown privilege");
+        return null;
+    }
+  }
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
index 21fa65d99..a997a95ce 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/TestableCommandLine.java
@@ -50,6 +50,7 @@ import org.apache.gravitino.cli.commands.DeleteTag;
 import org.apache.gravitino.cli.commands.DeleteTopic;
 import org.apache.gravitino.cli.commands.DeleteUser;
 import org.apache.gravitino.cli.commands.FilesetDetails;
+import org.apache.gravitino.cli.commands.GrantPrivilegesToRole;
 import org.apache.gravitino.cli.commands.GroupAudit;
 import org.apache.gravitino.cli.commands.GroupDetails;
 import org.apache.gravitino.cli.commands.ListAllTags;
@@ -85,6 +86,7 @@ import org.apache.gravitino.cli.commands.RemoveSchemaProperty;
 import org.apache.gravitino.cli.commands.RemoveTableProperty;
 import org.apache.gravitino.cli.commands.RemoveTagProperty;
 import org.apache.gravitino.cli.commands.RemoveTopicProperty;
+import org.apache.gravitino.cli.commands.RevokePrivilegesFromRole;
 import org.apache.gravitino.cli.commands.RoleAudit;
 import org.apache.gravitino.cli.commands.RoleDetails;
 import org.apache.gravitino.cli.commands.SchemaAudit;
@@ -862,4 +864,24 @@ public class TestableCommandLine {
       String comment) {
     return new CreateTable(url, ignore, metalake, catalog, schema, table, 
columnFile, comment);
   }
+
+  protected GrantPrivilegesToRole newGrantPrivilegesToRole(
+      String url,
+      boolean ignore,
+      String metalake,
+      String role,
+      FullName entity,
+      String[] privileges) {
+    return new GrantPrivilegesToRole(url, ignore, metalake, role, entity, 
privileges);
+  }
+
+  protected RevokePrivilegesFromRole newRevokePrivilegesFromRole(
+      String url,
+      boolean ignore,
+      String metalake,
+      String role,
+      FullName entity,
+      String[] privileges) {
+    return new RevokePrivilegesFromRole(url, ignore, metalake, role, entity, 
privileges);
+  }
 }
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GrantPrivilegesToRole.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GrantPrivilegesToRole.java
new file mode 100644
index 000000000..e3c9fa494
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/GrantPrivilegesToRole.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cli.commands;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.cli.ErrorMessages;
+import org.apache.gravitino.cli.FullName;
+import org.apache.gravitino.cli.Privileges;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.dto.authorization.PrivilegeDTO;
+import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchRoleException;
+
+/** Grants one or more privileges. */
+public class GrantPrivilegesToRole extends MetadataCommand {
+
+  protected final String metalake;
+  protected final String role;
+  protected final FullName entity;
+  protected final String[] privileges;
+
+  /**
+   * Grants one or more privileges.
+   *
+   * @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 role The name of the role.
+   * @param entity The name of the entity.
+   * @param privileges The list of privileges.
+   */
+  public GrantPrivilegesToRole(
+      String url,
+      boolean ignoreVersions,
+      String metalake,
+      String role,
+      FullName entity,
+      String[] privileges) {
+    super(url, ignoreVersions);
+    this.metalake = metalake;
+    this.entity = entity;
+    this.role = role;
+    this.privileges = privileges;
+  }
+
+  /** Grants one or more privileges. */
+  @Override
+  public void handle() {
+    try {
+      GravitinoClient client = buildClient(metalake);
+      List<Privilege> privilegesList = new ArrayList<>();
+
+      for (String privilege : privileges) {
+        if (!Privileges.isValid(privilege)) {
+          System.err.println("Unknown privilege " + privilege);
+          return;
+        }
+        PrivilegeDTO privilegeDTO =
+            PrivilegeDTO.builder()
+                .withName(Privileges.toName(privilege))
+                .withCondition(Privilege.Condition.ALLOW)
+                .build();
+        privilegesList.add(privilegeDTO);
+      }
+
+      MetadataObject metadataObject = constructMetadataObject(entity, client);
+      client.grantPrivilegesToRole(role, metadataObject, privilegesList);
+    } catch (NoSuchMetalakeException err) {
+      System.err.println(ErrorMessages.UNKNOWN_METALAKE);
+      return;
+    } catch (NoSuchRoleException err) {
+      System.err.println(ErrorMessages.UNKNOWN_ROLE);
+      return;
+    } catch (NoSuchMetadataObjectException err) {
+      System.err.println(ErrorMessages.UNKNOWN_USER);
+      return;
+    } catch (Exception exp) {
+      System.err.println(exp.getMessage());
+      return;
+    }
+
+    String all = String.join(",", privileges);
+    System.out.println(role + " granted " + all + " on " + entity.getName());
+  }
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetadataCommand.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetadataCommand.java
new file mode 100644
index 000000000..3f1e347c1
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/MetadataCommand.java
@@ -0,0 +1,83 @@
+/*
+ * 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.cli.commands;
+
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.cli.FullName;
+import org.apache.gravitino.client.GravitinoClient;
+
+public class MetadataCommand extends Command {
+
+  /**
+   * MetadataCommand constructor.
+   *
+   * @param url The URL of the Gravitino server.
+   * @param ignoreVersions If true don't check the client/server versions 
match.
+   */
+  public MetadataCommand(String url, boolean ignoreVersions) {
+    super(url, ignoreVersions);
+  }
+
+  /**
+   * Constructs a {@link MetadataObject} based on the provided client, 
existing metadata object, and
+   * entity name.
+   *
+   * @param entity The name of the entity.
+   * @param client The Gravitino client.
+   * @return A MetadataObject of the appropriate type.
+   * @throws IllegalArgumentException if the entity type cannot be determined 
or is unknown.
+   */
+  protected MetadataObject constructMetadataObject(FullName entity, 
GravitinoClient client) {
+
+    MetadataObject metadataObject;
+    String name = entity.getName();
+
+    if (entity.hasColumnName()) {
+      metadataObject = MetadataObjects.of(null, name, 
MetadataObject.Type.COLUMN);
+    } else if (entity.hasTableName()) {
+      Catalog catalog = client.loadCatalog(entity.getCatalogName());
+      Catalog.Type catalogType = catalog.type();
+      if (catalogType == Catalog.Type.RELATIONAL) {
+        metadataObject = MetadataObjects.of(null, name, 
MetadataObject.Type.TABLE);
+      } else if (catalogType == Catalog.Type.MESSAGING) {
+        metadataObject = MetadataObjects.of(null, name, 
MetadataObject.Type.TOPIC);
+      } else if (catalogType == Catalog.Type.FILESET) {
+        metadataObject = MetadataObjects.of(null, name, 
MetadataObject.Type.FILESET);
+      } else {
+        throw new IllegalArgumentException("Unknown entity type: " + name);
+      }
+    } else if (entity.hasSchemaName()) {
+      metadataObject = MetadataObjects.of(null, name, 
MetadataObject.Type.SCHEMA);
+    } else if (entity.hasCatalogName()) {
+      metadataObject = MetadataObjects.of(null, name, 
MetadataObject.Type.CATALOG);
+    } else if (entity.getMetalakeName() != null) {
+      metadataObject = MetadataObjects.of(null, name, 
MetadataObject.Type.METALAKE);
+    } else {
+      throw new IllegalArgumentException("Unknown entity type: " + name);
+    }
+    return metadataObject;
+  }
+
+  /* Do nothing, as parent will override. */
+  @Override
+  public void handle() {}
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RevokePrivilegesFromRole.java
 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RevokePrivilegesFromRole.java
new file mode 100644
index 000000000..807753231
--- /dev/null
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RevokePrivilegesFromRole.java
@@ -0,0 +1,106 @@
+/*
+ * 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.cli.commands;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.cli.ErrorMessages;
+import org.apache.gravitino.cli.FullName;
+import org.apache.gravitino.cli.Privileges;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.dto.authorization.PrivilegeDTO;
+import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
+import org.apache.gravitino.exceptions.NoSuchRoleException;
+
+/** Revokes one or more privileges. */
+public class RevokePrivilegesFromRole extends MetadataCommand {
+
+  protected final String metalake;
+  protected final String role;
+  protected final FullName entity;
+  protected final String[] privileges;
+
+  /**
+   * Revokes one or more privileges.
+   *
+   * @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 role The name of the role.
+   * @param entity The name of the entity.
+   * @param privileges The list of privileges.
+   */
+  public RevokePrivilegesFromRole(
+      String url,
+      boolean ignoreVersions,
+      String metalake,
+      String role,
+      FullName entity,
+      String[] privileges) {
+    super(url, ignoreVersions);
+    this.metalake = metalake;
+    this.entity = entity;
+    this.role = role;
+    this.privileges = privileges;
+  }
+
+  /** Revokes One or more privileges. */
+  @Override
+  public void handle() {
+    try {
+      GravitinoClient client = buildClient(metalake);
+      List<Privilege> privilegesList = new ArrayList<>();
+
+      for (String privilege : privileges) {
+        if (!Privileges.isValid(privilege)) {
+          System.err.println("Unknown privilege " + privilege);
+          return;
+        }
+        PrivilegeDTO privilegeDTO =
+            PrivilegeDTO.builder()
+                .withName(Privileges.toName(privilege))
+                .withCondition(Privilege.Condition.DENY)
+                .build();
+        privilegesList.add(privilegeDTO);
+      }
+
+      MetadataObject metadataObject = constructMetadataObject(entity, client);
+      client.revokePrivilegesFromRole(role, metadataObject, privilegesList);
+    } catch (NoSuchMetalakeException err) {
+      System.err.println(ErrorMessages.UNKNOWN_METALAKE);
+      return;
+    } catch (NoSuchRoleException err) {
+      System.err.println(ErrorMessages.UNKNOWN_ROLE);
+      return;
+    } catch (NoSuchMetadataObjectException err) {
+      System.err.println(ErrorMessages.UNKNOWN_USER);
+      return;
+    } catch (Exception exp) {
+      System.err.println(exp.getMessage());
+      return;
+    }
+
+    String all = String.join(",", privileges);
+    System.out.println(role + " revoked " + all + " on " + entity.getName());
+  }
+}
diff --git 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java
index 613ee60d2..2c1613ede 100644
--- 
a/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java
+++ 
b/clients/cli/src/main/java/org/apache/gravitino/cli/commands/RoleDetails.java
@@ -20,7 +20,7 @@
 package org.apache.gravitino.cli.commands;
 
 import java.util.List;
-import java.util.stream.Collectors;
+import org.apache.gravitino.authorization.Privilege;
 import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.cli.ErrorMessages;
 import org.apache.gravitino.client.GravitinoClient;
@@ -65,9 +65,12 @@ public class RoleDetails extends Command {
       return;
     }
 
-    // TODO expand in securable objects PR
-    String all = 
objects.stream().map(SecurableObject::name).collect(Collectors.joining(","));
-
-    System.out.println(all.toString());
+    for (SecurableObject object : objects) {
+      System.out.print(object.name() + "," + object.type() + ",");
+      for (Privilege privilege : object.privileges()) {
+        System.out.print(privilege.simpleString() + " ");
+      }
+    }
+    System.out.println("");
   }
 }
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestPrivileges.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestPrivileges.java
new file mode 100644
index 000000000..b6d39cade
--- /dev/null
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestPrivileges.java
@@ -0,0 +1,52 @@
+/*
+ * 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.cli;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+class TestsPrivileges {
+
+  @Test
+  void testValidPrivilege() {
+    assertTrue(Privileges.isValid(Privileges.CREATE_CATALOG));
+    assertTrue(Privileges.isValid(Privileges.CREATE_TABLE));
+    assertTrue(Privileges.isValid(Privileges.CONSUME_TOPIC));
+    assertTrue(Privileges.isValid(Privileges.MANAGE_GRANTS));
+  }
+
+  @Test
+  void testInvalidPrivilege() {
+    assertFalse(Privileges.isValid("non_existent_privilege"));
+    assertFalse(Privileges.isValid("create_database"));
+  }
+
+  @Test
+  void testNullPrivilege() {
+    assertFalse(Privileges.isValid(null));
+  }
+
+  @Test
+  void testEmptyPrivilege() {
+    assertFalse(Privileges.isValid(""));
+  }
+}
diff --git 
a/clients/cli/src/test/java/org/apache/gravitino/cli/TestRoleCommands.java 
b/clients/cli/src/test/java/org/apache/gravitino/cli/TestRoleCommands.java
index 179dba14f..88b380d63 100644
--- a/clients/cli/src/test/java/org/apache/gravitino/cli/TestRoleCommands.java
+++ b/clients/cli/src/test/java/org/apache/gravitino/cli/TestRoleCommands.java
@@ -19,7 +19,9 @@
 
 package org.apache.gravitino.cli;
 
+import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.eq;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.verify;
@@ -29,7 +31,9 @@ import org.apache.commons.cli.CommandLine;
 import org.apache.commons.cli.Options;
 import org.apache.gravitino.cli.commands.CreateRole;
 import org.apache.gravitino.cli.commands.DeleteRole;
+import org.apache.gravitino.cli.commands.GrantPrivilegesToRole;
 import org.apache.gravitino.cli.commands.ListRoles;
+import org.apache.gravitino.cli.commands.RevokePrivilegesFromRole;
 import org.apache.gravitino.cli.commands.RoleAudit;
 import org.apache.gravitino.cli.commands.RoleDetails;
 import org.junit.jupiter.api.BeforeEach;
@@ -152,4 +156,62 @@ class TestRoleCommands {
     commandLine.handleCommandLine();
     verify(mockDelete).handle();
   }
+
+  @Test
+  void testGrantPrivilegesToRole() {
+    GrantPrivilegesToRole mockGrant = mock(GrantPrivilegesToRole.class);
+    String[] privileges = {"create_table", "modify_table"};
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog");
+    when(mockCommandLine.hasOption(GravitinoOptions.ROLE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.ROLE)).thenReturn("admin");
+    
when(mockCommandLine.hasOption(GravitinoOptions.PRIVILEGE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValues(GravitinoOptions.PRIVILEGE)).thenReturn(privileges);
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.ROLE, 
CommandActions.GRANT));
+    doReturn(mockGrant)
+        .when(commandLine)
+        .newGrantPrivilegesToRole(
+            eq(GravitinoCommandLine.DEFAULT_URL),
+            eq(false),
+            eq("metalake_demo"),
+            eq("admin"),
+            any(),
+            eq(privileges));
+    commandLine.handleCommandLine();
+    verify(mockGrant).handle();
+  }
+
+  @Test
+  void testRevokePrivilegesFromRole() {
+    RevokePrivilegesFromRole mockRevoke = mock(RevokePrivilegesFromRole.class);
+    String[] privileges = {"create_table", "modify_table"};
+    
when(mockCommandLine.hasOption(GravitinoOptions.METALAKE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.METALAKE)).thenReturn("metalake_demo");
+    when(mockCommandLine.hasOption(GravitinoOptions.NAME)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.NAME)).thenReturn("catalog");
+    when(mockCommandLine.hasOption(GravitinoOptions.ROLE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValue(GravitinoOptions.ROLE)).thenReturn("admin");
+    
when(mockCommandLine.hasOption(GravitinoOptions.PRIVILEGE)).thenReturn(true);
+    
when(mockCommandLine.getOptionValues(GravitinoOptions.PRIVILEGE)).thenReturn(privileges);
+    GravitinoCommandLine commandLine =
+        spy(
+            new GravitinoCommandLine(
+                mockCommandLine, mockOptions, CommandEntities.ROLE, 
CommandActions.REVOKE));
+    doReturn(mockRevoke)
+        .when(commandLine)
+        .newRevokePrivilegesFromRole(
+            eq(GravitinoCommandLine.DEFAULT_URL),
+            eq(false),
+            eq("metalake_demo"),
+            eq("admin"),
+            any(),
+            eq(privileges));
+    commandLine.handleCommandLine();
+    verify(mockRevoke).handle();
+  }
 }
diff --git a/docs/cli.md b/docs/cli.md
index b01ea4775..e6e2f5aa6 100644
--- a/docs/cli.md
+++ b/docs/cli.md
@@ -672,6 +672,12 @@ gcli catalog set --owner --group groupA --name postgres
 
 ### Role commands
 
+When granting or revoking privileges the following privileges can be used.
+
+create_catalog, use_catalog, create_schema, use_schema, create_table, 
modify_table, select_table, create_fileset, write_fileset, read_fileset, 
create_topic, produce_topic, consume_topic, manage_users, create_role, 
manage_grants
+
+Note that some are only valid for certain entities.
+
 #### Display role details
 
 ```bash
@@ -721,10 +727,23 @@ gcli group grant --group groupA --role admin
 ```
 
 #### Remove a role from a group
+
 ```bash
 gcli group revoke --group groupA --role admin
 ```
 
+### Grant a privilege
+
+```bash
+gcli role grant --name catalog_postgres --role admin --privilege create_table 
modify_table
+```
+
+### Revoke a privilege
+
+```bash
+gcli role revoke --metalake metalake_demo --name catalog_postgres --role admin 
--privilege create_table modify_table
+```
+
 ### Topic commands
 
 #### Display a topic's details

Reply via email to