Skip to content

Commit

Permalink
[#6278] improve(CLI): Revoke all privileges from a role via the Gravi…
Browse files Browse the repository at this point in the history
…tino CLI. (#6293)

### What changes were proposed in this pull request?

Revoke all privileges from a role via the Gravitino CLI. syntax: `gcli
role revoke --name <name> --role <role> --all`

The step as follows:

1. Retrieve the `SecurableObject` instance using `role` and `fullname`;
2. Obtain the privileges associated with the `SecurableObject`;
3. Revoke all identified privileges;

### Why are the changes needed?

Fix: #6278 

### Does this PR introduce _any_ user-facing change?

No

### How was this patch tested?

local tests + ut

```bash
gcli role grant -m demo_metalake --name Hive.default --role roleA --privilege create_table modify_table -i
# roleA granted create_table,modify_table on Hive.default

gcli role revoke -m demo_metalake --name Hive.default --all --role roleA -i
# Hive.default: ALLOW create table, ALLOW modify table
```
  • Loading branch information
Abyss-lord authored Jan 17, 2025
1 parent 07f41a1 commit 6fb45d6
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -137,11 +137,19 @@ url, ignore, metalake, getOneRole(), new FullName(line), privileges)
}

private void handleRevokeCommand() {
gravitinoCommandLine
.newRevokePrivilegesFromRole(
url, ignore, metalake, getOneRole(), new FullName(line), privileges)
.validate()
.handle();
boolean removeAll = line.hasOption(GravitinoOptions.ALL);
if (removeAll) {
gravitinoCommandLine
.newRevokeAllPrivileges(url, ignore, metalake, getOneRole(), new FullName(line))
.validate()
.handle();
} else {
gravitinoCommandLine
.newRevokePrivilegesFromRole(
url, ignore, metalake, getOneRole(), new FullName(line), privileges)
.validate()
.handle();
}
}

private String getOneRole() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
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.RevokeAllPrivileges;
import org.apache.gravitino.cli.commands.RevokePrivilegesFromRole;
import org.apache.gravitino.cli.commands.RoleAudit;
import org.apache.gravitino.cli.commands.RoleDetails;
Expand Down Expand Up @@ -901,6 +902,11 @@ protected RevokePrivilegesFromRole newRevokePrivilegesFromRole(
return new RevokePrivilegesFromRole(url, ignore, metalake, role, entity, privileges);
}

protected RevokeAllPrivileges newRevokeAllPrivileges(
String url, boolean ignore, String metalake, String role, FullName entity) {
return new RevokeAllPrivileges(url, ignore, metalake, role, entity);
}

protected MetalakeEnable newMetalakeEnable(
String url, boolean ignore, String metalake, boolean enableAllCatalogs) {
return new MetalakeEnable(url, ignore, metalake, enableAllCatalogs);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/*
* 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 com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.authorization.Privilege;
import org.apache.gravitino.authorization.Role;
import org.apache.gravitino.authorization.SecurableObject;
import org.apache.gravitino.cli.ErrorMessages;
import org.apache.gravitino.cli.FullName;
import org.apache.gravitino.client.GravitinoClient;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.exceptions.NoSuchRoleException;

/** Revokes all privileges from a role to an entity or all entities. */
public class RevokeAllPrivileges extends MetadataCommand {

protected final String metalake;
protected final String role;
protected final FullName entity;

/**
* Revokes all privileges from a role to an entity or all entities.
*
* @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.
*/
public RevokeAllPrivileges(
String url, boolean ignoreVersions, String metalake, String role, FullName entity) {
super(url, ignoreVersions);
this.metalake = metalake;
this.role = role;
this.entity = entity;
}

/** Revokes all privileges from a role to an entity or all entities. */
@Override
public void handle() {
List<SecurableObject> matchedObjects;
Map<String, Set<Privilege>> revokedPrivileges = Maps.newHashMap();

try {
GravitinoClient client = buildClient(metalake);
matchedObjects = getMatchedObjects(client);

for (SecurableObject securableObject : matchedObjects) {
String objectFullName = securableObject.fullName();
Set<Privilege> privileges = new HashSet<>(securableObject.privileges());
MetadataObject metadataObject = constructMetadataObject(entity, client);
client.revokePrivilegesFromRole(role, metadataObject, privileges);

revokedPrivileges.put(objectFullName, privileges);
}
} catch (NoSuchMetalakeException e) {
exitWithError(ErrorMessages.UNKNOWN_METALAKE);
} catch (NoSuchRoleException e) {
exitWithError(ErrorMessages.UNKNOWN_ROLE);
} catch (Exception e) {
exitWithError(e.getMessage());
}

if (revokedPrivileges.isEmpty()) outputRevokedPrivileges("No privileges revoked.");
outputRevokedPrivileges(revokedPrivileges);
}

private List<SecurableObject> getMatchedObjects(GravitinoClient client) {
Role gRole = client.getRole(role);
return gRole.securableObjects().stream()
.filter(s -> s.fullName().equals(entity.getName()))
.collect(Collectors.toList());
}

private void outputRevokedPrivileges(Map<String, Set<Privilege>> revokedPrivileges) {
List<String> revokedInfoList = Lists.newArrayList();

for (Map.Entry<String, Set<Privilege>> entry : revokedPrivileges.entrySet()) {
List<String> revokedPrivilegesList =
entry.getValue().stream().map(Privilege::simpleString).collect(Collectors.toList());
revokedInfoList.add(entry.getKey() + ": " + COMMA_JOINER.join(revokedPrivilegesList));
}

System.out.println("Revoked privileges:");
for (String info : revokedInfoList) {
System.out.println(info);
}
}

private void outputRevokedPrivileges(String message) {
System.out.println(message);
}

/**
* verify the arguments.
*
* @return Returns itself via argument validation, otherwise exits.
*/
@Override
public Command validate() {
if (!entity.hasName()) exitWithError(ErrorMessages.MISSING_NAME);
return super.validate();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,17 +87,13 @@ public void handle() {
MetadataObject metadataObject = constructMetadataObject(entity, client);
client.revokePrivilegesFromRole(role, metadataObject, privilegesSet);
} catch (NoSuchMetalakeException err) {
System.err.println(ErrorMessages.UNKNOWN_METALAKE);
return;
exitWithError(ErrorMessages.UNKNOWN_METALAKE);
} catch (NoSuchRoleException err) {
System.err.println(ErrorMessages.UNKNOWN_ROLE);
return;
exitWithError(ErrorMessages.UNKNOWN_ROLE);
} catch (NoSuchMetadataObjectException err) {
System.err.println(ErrorMessages.UNKNOWN_USER);
return;
exitWithError(ErrorMessages.UNKNOWN_USER);
} catch (Exception exp) {
System.err.println(exp.getMessage());
return;
exitWithError(exp.getMessage());
}

String all = String.join(",", privileges);
Expand Down
3 changes: 3 additions & 0 deletions clients/cli/src/main/resources/role_help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,6 @@ gcli role grant --name catalog_postgres --role admin --privilege create_table mo

Revoke a privilege
gcli role revoke --metalake metalake_demo --name catalog_postgres --role admin --privilege create_table modify_table

Revoke all privileges
gcli role revoke --metalake metalake_demo --name catalog_postgres --role admin --all
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,14 @@
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
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.RevokeAllPrivileges;
import org.apache.gravitino.cli.commands.RevokePrivilegesFromRole;
import org.apache.gravitino.cli.commands.RoleAudit;
import org.apache.gravitino.cli.commands.RoleDetails;
Expand All @@ -48,6 +51,7 @@

class TestRoleCommands {
private CommandLine mockCommandLine;
private Options options;
private Options mockOptions;
private final ByteArrayOutputStream outContent = new ByteArrayOutputStream();
private final ByteArrayOutputStream errContent = new ByteArrayOutputStream();
Expand All @@ -58,6 +62,7 @@ class TestRoleCommands {
void setUp() {
mockCommandLine = mock(CommandLine.class);
mockOptions = mock(Options.class);
options = new GravitinoOptions().options();
System.setOut(new PrintStream(outContent));
System.setErr(new PrintStream(errContent));
}
Expand Down Expand Up @@ -99,6 +104,7 @@ void testRoleDetailsCommand() {
doReturn(mockDetails)
.when(commandLine)
.newRoleDetails(GravitinoCommandLine.DEFAULT_URL, false, "metalake_demo", "admin");

doReturn(mockDetails).when(mockDetails).validate();
commandLine.handleCommandLine();
verify(mockDetails).handle();
Expand Down Expand Up @@ -333,6 +339,51 @@ void testRevokePrivilegesFromRole() {
verify(mockRevoke).handle();
}

@Test
void testRevokeAllPrivilegesFromRole() {
RevokeAllPrivileges mockRevoke = mock(RevokeAllPrivileges.class);
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.getOptionValues(GravitinoOptions.ROLE)).thenReturn(new String[] {"admin"});
when(mockCommandLine.hasOption(GravitinoOptions.ALL)).thenReturn(true);
GravitinoCommandLine commandLine =
spy(
new GravitinoCommandLine(
mockCommandLine, mockOptions, CommandEntities.ROLE, CommandActions.REVOKE));
doReturn(mockRevoke)
.when(commandLine)
.newRevokeAllPrivileges(
eq(GravitinoCommandLine.DEFAULT_URL),
eq(false),
eq("metalake_demo"),
eq("admin"),
any());
doReturn(mockRevoke).when(mockRevoke).validate();
commandLine.handleCommandLine();
verify(mockRevoke).handle();
}

@Test
void testRevokeAllPrivilegesFromRoleWithoutName() throws ParseException {
Main.useExit = false;
String[] args = {"role", "revoke", "-m", "metalake_demo", "--role", "admin", "--all"};
CommandLine commandLine = new DefaultParser().parse(options, args);
FullName fullName = new FullName(commandLine);

RevokeAllPrivileges spyRevokeAllPrivileges =
spy(
new RevokeAllPrivileges(
GravitinoCommandLine.DEFAULT_URL, false, "metalake_demo", "admin", fullName));

assertThrows(RuntimeException.class, spyRevokeAllPrivileges::validate);
verify(spyRevokeAllPrivileges, never()).handle();
String errOutput = new String(errContent.toByteArray(), StandardCharsets.UTF_8).trim();
assertEquals(ErrorMessages.MISSING_NAME, errOutput);
}

@Test
void testRevokePrivilegesFromRoleWithoutPrivileges() {
Main.useExit = false;
Expand Down
6 changes: 6 additions & 0 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,12 @@ gcli role grant --name catalog_postgres --role admin --privilege create_table mo
gcli role revoke --metalake metalake_demo --name catalog_postgres --role admin --privilege create_table modify_table
```

### Revoke all privileges

```bash
gcli role revoke --metalake metalake_demo --name catalog_postgres --role admin --all
```

### Topic commands

#### Display a topic's details
Expand Down

0 comments on commit 6fb45d6

Please sign in to comment.