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 ba3953fb16 [#9269] feat(authz): Supports to override privileges (#9649)
ba3953fb16 is described below
commit ba3953fb16c5bb4e6988e02d9f3b1695bc4593be
Author: roryqi <[email protected]>
AuthorDate: Mon Jan 12 15:51:19 2026 +0800
[#9269] feat(authz): Supports to override privileges (#9649)
### What changes were proposed in this pull request?
Supports to override privileges
### Why are the changes needed?
Fix: #9269
### Does this PR introduce _any_ user-facing change?
No.
### How was this patch tested?
Added UTs.
---
.../dto/requests/PrivilegeOverrideRequest.java | 74 ++++++++++++
.../authorization/AccessControlDispatcher.java | 4 +
.../authorization/AccessControlManager.java | 11 ++
.../gravitino/authorization/PermissionManager.java | 131 +++++++++++++++++++++
.../hook/AccessControlHookDispatcher.java | 10 ++
.../listener/AccessControlEventDispatcher.java | 27 +++++
.../listener/api/event/OperationType.java | 1 +
.../api/event/OverridePrivilegesEvent.java | 82 +++++++++++++
.../api/event/OverridePrivilegesFailureEvent.java | 89 ++++++++++++++
.../api/event/OverridePrivilegesPreEvent.java | 83 +++++++++++++
.../TestAccessControlManagerForPermissions.java | 59 ++++++++++
docs/open-api/openapi.yaml | 3 +
docs/open-api/permissions.yaml | 72 +++++++++++
docs/security/access-control.md | 50 +++++++-
.../server/web/rest/PermissionOperations.java | 57 +++++++++
.../server/web/rest/TestPermissionOperations.java | 105 +++++++++++++++++
16 files changed, 856 insertions(+), 2 deletions(-)
diff --git
a/common/src/main/java/org/apache/gravitino/dto/requests/PrivilegeOverrideRequest.java
b/common/src/main/java/org/apache/gravitino/dto/requests/PrivilegeOverrideRequest.java
new file mode 100644
index 0000000000..67aa4dbfc2
--- /dev/null
+++
b/common/src/main/java/org/apache/gravitino/dto/requests/PrivilegeOverrideRequest.java
@@ -0,0 +1,74 @@
+/*
+ * 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.dto.requests;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.base.Preconditions;
+import lombok.Builder;
+import lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import lombok.extern.jackson.Jacksonized;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.gravitino.dto.authorization.SecurableObjectDTO;
+import org.apache.gravitino.rest.RESTRequest;
+
+/** Represents a request to update a role by overriding its securable objects.
*/
+@Getter
+@EqualsAndHashCode
+@ToString
+@Builder
+@Jacksonized
+public class PrivilegeOverrideRequest implements RESTRequest {
+
+ @JsonProperty("overrides")
+ private SecurableObjectDTO[] overrides;
+
+ /** Default constructor for PrivilegeOverrideRequest. (Used for Jackson
deserialization.) */
+ public PrivilegeOverrideRequest() {
+ this(null);
+ }
+
+ /**
+ * Creates a new PrivilegeOverrideRequest.
+ *
+ * @param overrides The securable objects to override for the role.
+ */
+ public PrivilegeOverrideRequest(SecurableObjectDTO[] overrides) {
+ this.overrides = overrides;
+ }
+
+ /**
+ * Validates the {@link PrivilegeOverrideRequest} request.
+ *
+ * @throws IllegalArgumentException If the request is invalid, this
exception is thrown.
+ */
+ @Override
+ public void validate() throws IllegalArgumentException {
+ for (SecurableObjectDTO objectDTO : overrides) {
+ Preconditions.checkArgument(
+ StringUtils.isNotBlank(objectDTO.name()), "\"securable object name\"
cannot be blank");
+ Preconditions.checkArgument(
+ objectDTO.type() != null, "\"securable object type\" cannot be
null");
+ Preconditions.checkArgument(
+ objectDTO.privileges() != null && !objectDTO.privileges().isEmpty(),
+ "\"securable object privileges\" cannot be null or empty");
+ }
+ }
+}
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 c1a9037089..49771a1f13 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,10 @@ public interface AccessControlDispatcher {
*/
boolean deleteRole(String metalake, String role) throws
NoSuchMetalakeException;
+ Role overridePrivilegesInRole(
+ String metalake, String role, List<SecurableObject>
securableObjectsToOverride)
+ throws NoSuchRoleException, NoSuchMetalakeException;
+
/**
* Lists the role names.
*
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 aa7476534e..762044178a 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
@@ -247,4 +247,15 @@ public class AccessControlManager implements
AccessControlDispatcher {
LockType.WRITE,
() -> permissionManager.revokePrivilegesFromRole(metalake, role,
object, privileges));
}
+
+ @Override
+ public Role overridePrivilegesInRole(
+ String metalake, String role, List<SecurableObject>
securableObjectsToOverride)
+ throws NoSuchRoleException, NoSuchMetalakeException {
+ return TreeLockUtils.doWithTreeLock(
+ AuthorizationUtils.ofRole(metalake, role),
+ LockType.WRITE,
+ () ->
+ permissionManager.overridePrivilegesInRole(metalake, role,
securableObjectsToOverride));
+ }
}
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
b/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
index 138ee77f5a..fadb3463bc 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/PermissionManager.java
@@ -24,10 +24,12 @@ import static
org.apache.gravitino.authorization.AuthorizationUtils.USER_DOES_NO
import static
org.apache.gravitino.authorization.AuthorizationUtils.filterSecurableObjects;
import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.io.IOException;
import java.time.Instant;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -35,6 +37,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.exceptions.IllegalRoleException;
import org.apache.gravitino.exceptions.NoSuchEntityException;
import org.apache.gravitino.exceptions.NoSuchGroupException;
@@ -597,6 +600,134 @@ class PermissionManager {
}
}
+ Role overridePrivilegesInRole(
+ String metalake, String role, List<SecurableObject>
securableObjectsToOverride) {
+ try {
+ AuthorizationPluginCallbackWrapper authorizationCallbackWrapper =
+ new AuthorizationPluginCallbackWrapper();
+ Role updatedRole =
+ store.update(
+ AuthorizationUtils.ofRole(metalake, role),
+ RoleEntity.class,
+ Entity.EntityType.ROLE,
+ roleEntity -> {
+ List<SecurableObject> currentSecurableObjects =
+ Lists.newArrayList(roleEntity.securableObjects());
+
+ // This is used for recording the original state of the role
before update.
+ // We use this to find which objects existed before update
+ Map<MetadataObject, SecurableObject> originObjectMap =
Maps.newHashMap();
+ for (SecurableObject securableObject :
currentSecurableObjects) {
+ originObjectMap.put(
+ MetadataObjects.parse(securableObject.fullName(),
securableObject.type()),
+ securableObject);
+ }
+
+ // This is used for recording the updated state of the role
after update.
+ Map<MetadataObject, SecurableObject> updatedObjectMap =
Maps.newHashMap();
+ for (SecurableObject securableObject :
securableObjectsToOverride) {
+ updatedObjectMap.put(
+ MetadataObjects.parse(securableObject.fullName(),
securableObject.type()),
+ securableObject);
+ }
+
+ // These sets will be used for tracking which objects are
created, updated or
+ // deleted.
+ Set<MetadataObject> authzPluginCreatedObjects =
+ Sets.newHashSet(updatedObjectMap.keySet());
+ authzPluginCreatedObjects.removeAll(originObjectMap.keySet());
+
+ Set<MetadataObject> authzPluginUpdateObjects =
+ Sets.newHashSet(updatedObjectMap.keySet());
+ authzPluginUpdateObjects.retainAll(originObjectMap.keySet());
+
+ Set<MetadataObject> authzPluginDeletedObjects =
+ Sets.newHashSet(originObjectMap.keySet());
+ authzPluginDeletedObjects.removeAll(updatedObjectMap.keySet());
+
+ // We set authorization callback here, we won't execute this
callback in this
+ // place. We will execute the callback after we execute the
SQL transaction.
+ authorizationCallbackWrapper.setCallback(
+ () -> {
+ authzPluginCreatedObjects.forEach(
+ object -> {
+
AuthorizationUtils.callAuthorizationPluginForMetadataObject(
+ metalake,
+ object,
+ authorizationPlugin -> {
+ authorizationPlugin.onRoleUpdated(
+ roleEntity,
+ RoleChange.addSecurableObject(
+ role, updatedObjectMap.get(object)));
+ });
+ });
+ authzPluginUpdateObjects.forEach(
+ object -> {
+ SecurableObject existingObject =
originObjectMap.get(object);
+ SecurableObject newSecurableObject =
updatedObjectMap.get(object);
+ // If the updated role is the same as the existing
one, we don't
+ // need to call the authorization plugin.
+ if (existingObject != null
+ && newSecurableObject != null
+ && !existingObject
+ .privileges()
+ .equals(newSecurableObject.privileges())) {
+
AuthorizationUtils.callAuthorizationPluginForMetadataObject(
+ metalake,
+ object,
+ authorizationPlugin -> {
+ authorizationPlugin.onRoleUpdated(
+ roleEntity,
+ RoleChange.updateSecurableObject(
+ role, existingObject,
newSecurableObject));
+ });
+ }
+ });
+ authzPluginDeletedObjects.forEach(
+ object -> {
+
AuthorizationUtils.callAuthorizationPluginForMetadataObject(
+ metalake,
+ object,
+ authorizationPlugin -> {
+ authorizationPlugin.onRoleUpdated(
+ roleEntity,
+ RoleChange.removeSecurableObject(
+ role, originObjectMap.get(object)));
+ });
+ });
+ });
+
+ AuditInfo auditInfo =
+ AuditInfo.builder()
+ .withCreator(roleEntity.auditInfo().creator())
+ .withCreateTime(roleEntity.auditInfo().createTime())
+
.withLastModifier(PrincipalUtils.getCurrentPrincipal().getName())
+ .withLastModifiedTime(Instant.now())
+ .build();
+
+ return RoleEntity.builder()
+ .withId(roleEntity.id())
+ .withName(roleEntity.name())
+ .withNamespace(roleEntity.namespace())
+ .withProperties(roleEntity.properties())
+ .withAuditInfo(auditInfo)
+
.withSecurableObjects(Lists.newArrayList(updatedObjectMap.values()))
+ .build();
+ });
+ authorizationCallbackWrapper.execute();
+
+ return updatedRole;
+ } catch (IOException ioe) {
+ LOG.error(
+ "Updating role {} in the metalake {} failed due to storage issues",
role, metalake, ioe);
+ throw new RuntimeException(ioe);
+ } catch (NoSuchEntityException nse) {
+ LOG.error(
+ "Failed to override, role {} does not exist in the metalake {}",
role, metalake, nse);
+ throw new NoSuchRoleException(ROLE_DOES_NOT_EXIST_MSG, role, metalake);
+ }
+ }
+
private static SecurableObject createNewSecurableObject(
String metalake,
String role,
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 df6847544c..5cce42c4d0 100644
---
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
@@ -224,6 +224,16 @@ public class AccessControlHookDispatcher implements
AccessControlDispatcher {
return revokedRole;
}
+ @Override
+ public Role overridePrivilegesInRole(
+ String metalake, String role, List<SecurableObject>
securableObjectsToOverride)
+ throws NoSuchRoleException, NoSuchMetalakeException {
+ Role overriddenRole =
+ dispatcher.overridePrivilegesInRole(metalake, role,
securableObjectsToOverride);
+ notifyRoleUserRelChange(metalake, role);
+ return overriddenRole;
+ }
+
private static void notifyRoleUserRelChange(String metalake, List<String>
roles) {
GravitinoAuthorizer gravitinoAuthorizer =
GravitinoEnv.getInstance().gravitinoAuthorizer();
if (gravitinoAuthorizer != null) {
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 3ec137f191..2234672875 100644
---
a/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
+++
b/core/src/main/java/org/apache/gravitino/listener/AccessControlEventDispatcher.java
@@ -83,6 +83,9 @@ import
org.apache.gravitino.listener.api.event.ListUserNamesPreEvent;
import org.apache.gravitino.listener.api.event.ListUsersEvent;
import org.apache.gravitino.listener.api.event.ListUsersFailureEvent;
import org.apache.gravitino.listener.api.event.ListUsersPreEvent;
+import org.apache.gravitino.listener.api.event.OverridePrivilegesEvent;
+import org.apache.gravitino.listener.api.event.OverridePrivilegesFailureEvent;
+import org.apache.gravitino.listener.api.event.OverridePrivilegesPreEvent;
import org.apache.gravitino.listener.api.event.RemoveGroupEvent;
import org.apache.gravitino.listener.api.event.RemoveGroupFailureEvent;
import org.apache.gravitino.listener.api.event.RemoveGroupPreEvent;
@@ -521,4 +524,28 @@ public class AccessControlEventDispatcher implements
AccessControlDispatcher {
throw e;
}
}
+
+ @Override
+ public Role overridePrivilegesInRole(
+ String metalake, String role, List<SecurableObject>
securableObjectsToOverride)
+ throws NoSuchRoleException, NoSuchMetalakeException {
+ String initiator = PrincipalUtils.getCurrentUserName();
+
+ eventBus.dispatchEvent(
+ new OverridePrivilegesPreEvent(initiator, metalake, role,
securableObjectsToOverride));
+ try {
+ Role roleObject =
+ dispatcher.overridePrivilegesInRole(metalake, role,
securableObjectsToOverride);
+ eventBus.dispatchEvent(
+ new OverridePrivilegesEvent(
+ initiator, metalake, new RoleInfo(roleObject),
securableObjectsToOverride));
+
+ return roleObject;
+ } catch (Exception e) {
+ eventBus.dispatchEvent(
+ new OverridePrivilegesFailureEvent(
+ initiator, metalake, e, role, securableObjectsToOverride));
+ throw e;
+ }
+ }
}
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 dd1fa67e3c..b2ea3c8b3c 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
@@ -143,6 +143,7 @@ public enum OperationType {
LIST_ROLE_NAMES,
GRANT_PRIVILEGES,
REVOKE_PRIVILEGES,
+ OVERRIDE_PRIVILEGES,
// Owner operations
GET_OWNER,
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesEvent.java
new file mode 100644
index 0000000000..5ffa35b5bf
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesEvent.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.listener.api.event;
+
+import java.util.List;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.listener.api.info.RoleInfo;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/** Represents an event triggered after privileges are overridden in a role. */
+@DeveloperApi
+public class OverridePrivilegesEvent extends RoleEvent {
+ private final RoleInfo updatedRoleInfo;
+ private final List<SecurableObject> securableObjectsToOverride;
+
+ /**
+ * Constructs a new {@code OverridePrivilegesEvent} instance.
+ *
+ * @param initiator the user who initiated the event.
+ * @param metalake the metalake name where the event occurred.
+ * @param updatedRoleInfo the {@code RoleInfo} of the role that was updated
with overridden
+ * privileges.
+ * @param securableObjectsToOverride the list of securable objects that were
overridden in the
+ * role.
+ */
+ public OverridePrivilegesEvent(
+ String initiator,
+ String metalake,
+ RoleInfo updatedRoleInfo,
+ List<SecurableObject> securableObjectsToOverride) {
+ super(initiator, NameIdentifierUtil.ofRole(metalake,
updatedRoleInfo.roleName()));
+
+ this.updatedRoleInfo = updatedRoleInfo;
+ this.securableObjectsToOverride = securableObjectsToOverride;
+ }
+
+ /**
+ * Returns the role information of the role that was updated.
+ *
+ * @return the {@code RoleInfo} instance containing details of the updated
role.
+ */
+ public RoleInfo updatedRoleInfo() {
+ return updatedRoleInfo;
+ }
+
+ /**
+ * Returns the list of securable objects that were overridden.
+ *
+ * @return a list of {@code SecurableObject} instances.
+ */
+ public List<SecurableObject> securableObjectsToOverride() {
+ return securableObjectsToOverride;
+ }
+
+ /**
+ * Returns the operation type of this event.
+ *
+ * @return the operation type.
+ */
+ @Override
+ public OperationType operationType() {
+ return OperationType.OVERRIDE_PRIVILEGES;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesFailureEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesFailureEvent.java
new file mode 100644
index 0000000000..625a588e34
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesFailureEvent.java
@@ -0,0 +1,89 @@
+/*
+ * 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 com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/**
+ * Represents an event triggered when an attempt to override privileges in a
role fails due to an
+ * exception.
+ */
+@DeveloperApi
+public class OverridePrivilegesFailureEvent extends RoleFailureEvent {
+
+ private final String roleName;
+ private final List<SecurableObject> securableObjectsToOverride;
+
+ /**
+ * Constructs a new {@code OverridePrivilegesFailureEvent} instance.
+ *
+ * @param user the name of the user who triggered the event
+ * @param metalake the name of the metalake
+ * @param exception the exception that occurred while overriding privileges
+ * @param roleName the name of the role whose privileges were being
overridden
+ * @param securableObjectsToOverride the list of securable objects intended
to be overridden; if
+ * {@code null}, an empty list is used
+ */
+ public OverridePrivilegesFailureEvent(
+ String user,
+ String metalake,
+ Exception exception,
+ String roleName,
+ List<SecurableObject> securableObjectsToOverride) {
+ super(user, NameIdentifierUtil.ofRole(metalake, roleName), exception);
+ this.roleName = roleName;
+ this.securableObjectsToOverride =
+ securableObjectsToOverride == null
+ ? ImmutableList.of()
+ : ImmutableList.copyOf(securableObjectsToOverride);
+ }
+
+ /**
+ * Returns the name of the role.
+ *
+ * @return the name of the role whose privileges were intended to be
overridden
+ */
+ public String roleName() {
+ return roleName;
+ }
+
+ /**
+ * Returns the list of securable objects intended to be overridden.
+ *
+ * @return an immutable list of securable objects
+ */
+ public List<SecurableObject> securableObjectsToOverride() {
+ return securableObjectsToOverride;
+ }
+
+ /**
+ * Returns the operation type of this event.
+ *
+ * @return the operation type.
+ */
+ @Override
+ public OperationType operationType() {
+ return OperationType.OVERRIDE_PRIVILEGES;
+ }
+}
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesPreEvent.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesPreEvent.java
new file mode 100644
index 0000000000..68b965816b
--- /dev/null
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/OverridePrivilegesPreEvent.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.listener.api.event;
+
+import java.util.List;
+import org.apache.gravitino.annotation.DeveloperApi;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+
+/**
+ * Represents an event generated before overriding privileges in a role. This
class encapsulates the
+ * details of the privilege override event prior to its execution.
+ */
+@DeveloperApi
+public class OverridePrivilegesPreEvent extends RolePreEvent {
+ private final String roleName;
+ private final List<SecurableObject> securableObjectsToOverride;
+
+ /**
+ * Constructs a new {@link OverridePrivilegesPreEvent} instance with the
specified initiator,
+ * identifier, role name, and securable objects.
+ *
+ * @param initiator The name of the user who initiated the event.
+ * @param metalake The name of the metalake.
+ * @param roleName The name of the role whose privileges will be overridden.
+ * @param securableObjectsToOverride The list of securable objects to
override in the role.
+ */
+ public OverridePrivilegesPreEvent(
+ String initiator,
+ String metalake,
+ String roleName,
+ List<SecurableObject> securableObjectsToOverride) {
+ super(initiator, NameIdentifierUtil.ofRole(metalake, roleName));
+
+ this.roleName = roleName;
+ this.securableObjectsToOverride = securableObjectsToOverride;
+ }
+
+ /**
+ * Returns the name of the role.
+ *
+ * @return The name of the role whose privileges will be overridden.
+ */
+ public String roleName() {
+ return roleName;
+ }
+
+ /**
+ * Returns the list of securable objects to override.
+ *
+ * @return The list of securable objects to override in the role.
+ */
+ public List<SecurableObject> securableObjectsToOverride() {
+ return securableObjectsToOverride;
+ }
+
+ /**
+ * Returns the operation type of this event.
+ *
+ * @return the operation type.
+ */
+ @Override
+ public OperationType operationType() {
+ return OperationType.OVERRIDE_PRIVILEGES;
+ }
+}
diff --git
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManagerForPermissions.java
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManagerForPermissions.java
index e4b62567d4..4b9903538e 100644
---
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManagerForPermissions.java
+++
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManagerForPermissions.java
@@ -18,6 +18,9 @@
*/
package org.apache.gravitino.authorization;
+import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
+import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
+import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -46,6 +49,7 @@ import org.apache.gravitino.exceptions.NoSuchGroupException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.exceptions.NoSuchRoleException;
import org.apache.gravitino.exceptions.NoSuchUserException;
+import org.apache.gravitino.lock.LockManager;
import org.apache.gravitino.meta.AuditInfo;
import org.apache.gravitino.meta.BaseMetalake;
import org.apache.gravitino.meta.GroupEntity;
@@ -72,6 +76,7 @@ public class TestAccessControlManagerForPermissions {
private static String METALAKE = "metalake";
private static String CATALOG = "catalog";
+ private static String SCHEMA = "schema";
private static String USER = "user";
@@ -177,6 +182,11 @@ public class TestAccessControlManagerForPermissions {
.thenReturn(new NameIdentifier[] {NameIdentifier.of("metalake",
"catalog")});
authorizationPlugin = Mockito.mock(AuthorizationPlugin.class);
Mockito.when(catalog.getAuthorizationPlugin()).thenReturn(authorizationPlugin);
+
+ config.set(TREE_LOCK_MAX_NODE_IN_MEMORY, 100000L);
+ config.set(TREE_LOCK_MIN_NODE_IN_MEMORY, 1000L);
+ config.set(TREE_LOCK_CLEAN_INTERVAL, 36000L);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new
LockManager(config), true);
}
@AfterAll
@@ -426,4 +436,53 @@ public class TestAccessControlManagerForPermissions {
MetadataObjects.of(null, METALAKE,
MetadataObject.Type.METALAKE),
Sets.newHashSet(Privileges.CreateTable.allow())));
}
+
+ @Test
+ public void testOverridePrivileges() throws Exception {
+ String testRole = "role";
+ SecurableObject catalog =
+ SecurableObjects.ofCatalog(
+ CATALOG,
+ Lists.newArrayList(Privileges.UseCatalog.allow(),
Privileges.CreateTable.allow()));
+
+ SecurableObject schema =
+ SecurableObjects.ofSchema(
+ catalog, SCHEMA,
Lists.newArrayList(Privileges.CreateTable.allow()));
+
+ // Add two securable objects
+ Role role =
+ accessControlManager.overridePrivilegesInRole(
+ METALAKE, testRole, Lists.newArrayList(catalog, schema));
+
+ List<SecurableObject> objects = role.securableObjects();
+
+ Assertions.assertEquals(2, objects.size());
+
+ // Remove one securable object
+ role =
+ accessControlManager.overridePrivilegesInRole(
+ METALAKE, testRole, Lists.newArrayList(catalog));
+ objects = role.securableObjects();
+ Assertions.assertEquals(1, objects.size());
+ Assertions.assertEquals(catalog, objects.get(0));
+
+ // Update one securable object
+ SecurableObject catalogAnother =
+ SecurableObjects.ofCatalog(CATALOG,
Lists.newArrayList(Privileges.UseCatalog.allow()));
+ role =
+ accessControlManager.overridePrivilegesInRole(
+ METALAKE, testRole, Lists.newArrayList(catalogAnother));
+
+ objects = role.securableObjects();
+ Assertions.assertEquals(1, objects.size());
+ Assertions.assertEquals(catalogAnother, objects.get(0));
+
+ // Throw IllegalRoleException
+ String notExist = "not-exist";
+ Assertions.assertThrows(
+ NoSuchRoleException.class,
+ () ->
+ accessControlManager.overridePrivilegesInRole(
+ METALAKE, notExist, Lists.newArrayList()));
+ }
}
diff --git a/docs/open-api/openapi.yaml b/docs/open-api/openapi.yaml
index 0c634632be..8dd0f70274 100644
--- a/docs/open-api/openapi.yaml
+++ b/docs/open-api/openapi.yaml
@@ -200,6 +200,9 @@ paths:
/metalakes/{metalake}/permissions/roles/{role}/{metadataObjectType}/{metadataObjectFullName}/revoke:
$ref:
"./permissions.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1permissions~1roles~1%7Brole%7D~1%7BmetadataObjectType%7D~1%7BmetadataObjectFullName%7D~1revoke"
+ /metalakes/{metalake}/permissions/roles/{role}:
+ $ref:
"./permissions.yaml#/paths/~1metalakes~1%7Bmetalake%7D~1permissions~1roles~1%7Brole%7D"
+
/lineage:
$ref: "./lineage.yaml#/paths/~1lineage"
diff --git a/docs/open-api/permissions.yaml b/docs/open-api/permissions.yaml
index 98b271328b..c60be31460 100644
--- a/docs/open-api/permissions.yaml
+++ b/docs/open-api/permissions.yaml
@@ -357,6 +357,53 @@ paths:
"5xx":
$ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
+ /metalakes/{metalake}/permissions/roles/{role}:
+ parameters:
+ - $ref: "./openapi.yaml#/components/parameters/metalake"
+ - $ref: "./openapi.yaml#/components/parameters/role"
+ put:
+ tags:
+ - access control
+ summary: override role privileges
+ operationId: overrideRolePrivileges
+ description: |
+ Override the privileges of the bulk securable objects for a specific
role in a metalake.
+
+ requestBody:
+ content:
+ application/json:
+ schema:
+ $ref: "#/components/schemas/PrivilegeOverrideRequest"
+
+ responses:
+ "200":
+ description: Returns the updated role
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./roles.yaml#/components/responses/RoleResponse"
+ "400":
+ description: Parameter is invalid - The specified privilege is
invalid
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ IllegalPrivilegeException:
+ $ref: "#/components/examples/IllegalPrivilegeException"
+ "404":
+ description: Not Found - The target role does not exist
+ content:
+ application/vnd.gravitino.v1+json:
+ schema:
+ $ref: "./openapi.yaml#/components/schemas/ErrorModel"
+ examples:
+ NoSuchMetalakeException:
+ $ref:
"./metalakes.yaml#/components/examples/NoSuchMetalakeException"
+ NoSuchRoleException:
+ $ref: "./roles.yaml#/components/examples/NoSuchRoleException"
+ "5xx":
+ $ref: "./openapi.yaml#/components/responses/ServerErrorResponse"
components:
@@ -405,6 +452,15 @@ components:
items:
$ref: "./roles.yaml#/components/schemas/Privilege"
+ PrivilegeOverrideRequest:
+ type: object
+ properties:
+ overrides:
+ type: array
+ description: A list of securable objects to update
+ items:
+ $ref: "./roles.yaml#/components/schemas/SecurableObject"
+
examples:
RoleGrantRequest:
@@ -435,6 +491,22 @@ components:
} ]
}
+ PrivilegeOverrideRequest:
+ value: {
+ "overrides": [
+ {
+ "fullName": "catalog1.schema1.table1",
+ "type": "TABLE",
+ "privileges": [
+ {
+ name: "SELECT_TABLE",
+ condition: "ALLOW"
+ }
+ ]
+ }
+ ]
+ }
+
IllegalRoleException:
value: {
"code": 1001,
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index 5ff5397a58..0fee9d2a81 100644
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -914,6 +914,51 @@ Role role = client.revokePrivilegesFromRole("role1",
table, Lists.newArrayList(P
</TabItem>
</Tabs>
+### Override privileges in a role
+
+You can override all privileges in a role with a new set of securable objects
and their privileges. This operation completely replaces the role's entire
privilege configuration - any securable objects not included in the request
will be removed from the role.
+
+The request path for REST API is
`/api/metalakes/{metalake}/permissions/roles/{role}/`.
+
+```shell
+curl -X PUT -H "Accept: application/vnd.gravitino.v1+json" \
+-H "Content-Type: application/json" -d '{
+ "overrides": [
+ {
+ "fullName": "catalog1.schema1.table1",
+ "type": "TABLE",
+ "privileges": [
+ {
+ "name": "SELECT_TABLE",
+ "condition": "ALLOW"
+ }
+ ]
+ },
+ {
+ "fullName": "catalog1.schema1.table2",
+ "type": "TABLE",
+ "privileges": [
+ {
+ "name": "MODIFY_TABLE",
+ "condition": "ALLOW"
+ }
+ ]
+ }
+ ]
+}' http://localhost:8090/api/metalakes/test/permissions/roles/role1/
+```
+
+:::warning
+This operation completely replaces **all privileges** in the role. The role
will contain only the securable objects and privileges specified in the
request. Any securable objects that existed in the role but are not included in
the request will be removed from the role.
+
+**Example scenario:**
+- Before: `role1` has privileges on `table1`, `table2`, and `table3`
+- Request: Override with privileges for `table1` and `table4` only
+- After: `role1` has privileges only on `table1` and `table4` (table2 and
table3 are removed, table4 is added)
+
+Use this operation when you want to set the exact privilege configuration for
a role, replacing its entire state.
+:::
+
### Grant roles to a user
You can grant specific roles to a user.
@@ -1174,8 +1219,9 @@ The following table lists the required privileges for
each API.
| list roles | `MANAGE_GRANTS` on the metalake or the
owner of the metalake can see all the roles. Others can see his granted roles
or owned roles.
|
| grant role | `MANAGE_GRANTS` on the metalake
|
| revoke role | `MANAGE_GRANTS` on the metalake
|
-| grant privilege | `MANAGE_GRANTS` on the metalake or the
owner of the securable object
|
-| revoke privilege | `MANAGE_GRANTS` on the metalake or the
owner of the securable object
|
+| grant privilege | `MANAGE_GRANTS` on the metalake or the
owner of the securable object or the metalake
|
+| revoke privilege | `MANAGE_GRANTS` on the metalake or the
owner of the securable object or the metalake
|
+| override privilege | `MANAGE_GRANTS` on the metalake or the
owner of the metalake
|
| set owner | The owner of the securable object
|
| list tags | The owner of the metalake can see all
the tags, others can see the tags which they can load.
|
| create tag | `CREATE_TAG` on the metalake or the
owner of the metalake.
|
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
b/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
index a78c6e9976..7481d21ed3 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/rest/PermissionOperations.java
@@ -22,6 +22,8 @@ import static
org.apache.gravitino.server.authorization.expression.Authorization
import com.codahale.metrics.annotation.ResponseMetered;
import com.codahale.metrics.annotation.Timed;
+import com.google.common.collect.Lists;
+import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
@@ -38,8 +40,13 @@ import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.MetadataObjects;
import org.apache.gravitino.authorization.AccessControlDispatcher;
import org.apache.gravitino.authorization.AuthorizationUtils;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.authorization.SecurableObject;
+import org.apache.gravitino.authorization.SecurableObjects;
import org.apache.gravitino.dto.authorization.PrivilegeDTO;
+import org.apache.gravitino.dto.authorization.SecurableObjectDTO;
import org.apache.gravitino.dto.requests.PrivilegeGrantRequest;
+import org.apache.gravitino.dto.requests.PrivilegeOverrideRequest;
import org.apache.gravitino.dto.requests.PrivilegeRevokeRequest;
import org.apache.gravitino.dto.requests.RoleGrantRequest;
import org.apache.gravitino.dto.requests.RoleRevokeRequest;
@@ -274,4 +281,54 @@ public class PermissionOperations {
OperationType.REVOKE, fullName, role, e);
}
}
+
+ @PUT
+ @Path("roles/{role}/")
+ @Produces("application/vnd.gravitino.v1+json")
+ @Timed(name = "override-role-privileges." +
MetricNames.HTTP_PROCESS_DURATION, absolute = true)
+ @ResponseMetered(name = "override-role-privileges", absolute = true)
+ @AuthorizationExpression(expression = "METALAKE::OWNER ||
METALAKE::MANAGE_GRANTS")
+ public Response overrideRolePrivileges(
+ @PathParam("metalake") @AuthorizationMetadata(type =
Entity.EntityType.METALAKE)
+ String metalake,
+ @PathParam("role") String role,
+ PrivilegeOverrideRequest request) {
+ try {
+
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ request.validate();
+ // Check update objects
+ List<SecurableObject> updatedObjects = Lists.newArrayList();
+
+ for (SecurableObjectDTO dto : request.getOverrides()) {
+ for (Privilege privilegeDTO : dto.privileges()) {
+ AuthorizationUtils.checkPrivilege((PrivilegeDTO) privilegeDTO,
dto, metalake);
+ }
+
+ MetadataObjectUtil.checkMetadataObject(metalake, dto);
+
AuthorizationUtils.checkDuplicatedNamePrivilege(dto.privileges());
+
+ updatedObjects.add(
+ SecurableObjects.parse(
+ dto.fullName(),
+ dto.type(),
+ dto.privileges().stream()
+ .map(
+ privilege ->
DTOConverters.fromPrivilegeDTO((PrivilegeDTO) privilege))
+ .collect(Collectors.toList())));
+ }
+
+ return Utils.ok(
+ new RoleResponse(
+ DTOConverters.toDTO(
+ accessControlManager.overridePrivilegesInRole(
+ metalake, role, updatedObjects))));
+ });
+ } catch (Exception e) {
+ return ExceptionHandlers.handleRolePermissionOperationException(
+ OperationType.UPDATE, role, metalake, e);
+ }
+ }
}
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
index fdc4c2e2f0..ba7f1b4456 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestPermissionOperations.java
@@ -29,6 +29,7 @@ import static org.mockito.Mockito.when;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.time.Instant;
+import java.util.Collections;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Application;
@@ -37,15 +38,20 @@ import javax.ws.rs.core.Response;
import org.apache.commons.lang3.reflect.FieldUtils;
import org.apache.gravitino.Config;
import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.authorization.AccessControlManager;
import org.apache.gravitino.authorization.Group;
import org.apache.gravitino.authorization.Privilege;
import org.apache.gravitino.authorization.Privileges;
import org.apache.gravitino.authorization.Role;
+import org.apache.gravitino.authorization.SecurableObject;
import org.apache.gravitino.authorization.SecurableObjects;
import org.apache.gravitino.authorization.User;
+import org.apache.gravitino.catalog.TableDispatcher;
import org.apache.gravitino.dto.authorization.PrivilegeDTO;
+import org.apache.gravitino.dto.authorization.SecurableObjectDTO;
import org.apache.gravitino.dto.requests.PrivilegeGrantRequest;
+import org.apache.gravitino.dto.requests.PrivilegeOverrideRequest;
import org.apache.gravitino.dto.requests.PrivilegeRevokeRequest;
import org.apache.gravitino.dto.requests.RoleGrantRequest;
import org.apache.gravitino.dto.requests.RoleRevokeRequest;
@@ -56,6 +62,7 @@ import org.apache.gravitino.dto.responses.RoleResponse;
import org.apache.gravitino.dto.responses.UserResponse;
import org.apache.gravitino.exceptions.IllegalPrivilegeException;
import org.apache.gravitino.exceptions.IllegalRoleException;
+import org.apache.gravitino.exceptions.NoSuchMetadataObjectException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.exceptions.NoSuchUserException;
import org.apache.gravitino.lock.LockManager;
@@ -77,6 +84,7 @@ public class TestPermissionOperations extends
BaseOperationsTest {
private static final AccessControlManager manager =
mock(AccessControlManager.class);
private static final MetalakeDispatcher metalakeDispatcher =
mock(MetalakeDispatcher.class);
+ private static final TableDispatcher tableDispatcher =
mock(TableDispatcher.class);
private static class MockServletRequestFactory extends
ServletRequestFactoryBase {
@Override
@@ -97,6 +105,7 @@ public class TestPermissionOperations extends
BaseOperationsTest {
FieldUtils.writeField(GravitinoEnv.getInstance(),
"accessControlDispatcher", manager, true);
FieldUtils.writeField(
GravitinoEnv.getInstance(), "metalakeDispatcher", metalakeDispatcher,
true);
+ FieldUtils.writeField(GravitinoEnv.getInstance(), "tableDispatcher",
tableDispatcher, true);
}
@Override
@@ -644,4 +653,100 @@ public class TestPermissionOperations extends
BaseOperationsTest {
Assertions.assertEquals(
IllegalPrivilegeException.class.getSimpleName(),
wrongPriErrorResp.getType());
}
+
+ @Test
+ public void testOverridePrivileges() {
+ SecurableObjectDTO[] updates =
+ new SecurableObjectDTO[] {
+ SecurableObjectDTO.builder()
+ .withPrivileges(
+ new PrivilegeDTO[] {
+ PrivilegeDTO.builder()
+ .withName(Privilege.Name.SELECT_TABLE)
+ .withCondition(Privilege.Condition.ALLOW)
+ .build()
+ })
+ .withType(MetadataObject.Type.TABLE)
+ .withFullName("test1.test2.test3")
+ .build()
+ };
+
+ SecurableObject catalog =
+ SecurableObjects.ofCatalog("catalog",
Lists.newArrayList(Privileges.UseCatalog.allow()));
+ SecurableObject anotherSecurableObject =
+ SecurableObjects.ofCatalog(
+ "another_catalog",
Lists.newArrayList(Privileges.CreateSchema.deny()));
+
+ Role roleEntity =
+ RoleEntity.builder()
+ .withId(1L)
+ .withName("role1")
+ .withProperties(Collections.emptyMap())
+ .withSecurableObjects(Lists.newArrayList(catalog,
anotherSecurableObject))
+ .withAuditInfo(
+
AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build())
+ .build();
+
+ PrivilegeOverrideRequest req = new PrivilegeOverrideRequest(updates);
+
+ when(manager.overridePrivilegesInRole(any(), any(),
any())).thenReturn(roleEntity);
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+
+ Response resp =
+ target("/metalakes/metalake1/permissions/roles/role1")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.OK.getStatusCode(),
resp.getStatus());
+ Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE,
resp.getMediaType());
+
+ RoleResponse roleResponse = resp.readEntity(RoleResponse.class);
+ Assertions.assertEquals(0, roleResponse.getCode());
+ Role roleDTO = roleResponse.getRole();
+ Assertions.assertEquals("role1", roleDTO.name());
+ Assertions.assertTrue(roleDTO.properties().isEmpty());
+ Assertions.assertEquals(
+ SecurableObjects.ofCatalog("catalog",
Lists.newArrayList(Privileges.UseCatalog.allow()))
+ .fullName(),
+ roleDTO.securableObjects().get(0).fullName());
+ Assertions.assertEquals(1,
roleDTO.securableObjects().get(0).privileges().size());
+ Assertions.assertEquals(
+ Privileges.UseCatalog.allow().name(),
+ roleDTO.securableObjects().get(0).privileges().get(0).name());
+ Assertions.assertEquals(
+ Privileges.UseCatalog.allow().condition(),
+ roleDTO.securableObjects().get(0).privileges().get(0).condition());
+
+ // Throw no found exception
+ when(tableDispatcher.tableExists(any())).thenReturn(false);
+ Response resp1 =
+ target("/metalakes/metalake1/permissions/roles/role1/")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(),
resp1.getStatus());
+
+ ErrorResponse errorResponse = resp1.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE,
errorResponse.getCode());
+ Assertions.assertEquals(
+ NoSuchMetadataObjectException.class.getSimpleName(),
errorResponse.getType());
+
+ // Throw runtime exception
+ when(tableDispatcher.tableExists(any())).thenReturn(true);
+ when(manager.overridePrivilegesInRole(any(), any(), any()))
+ .thenThrow(new RuntimeException("Test exception"));
+ Response resp2 =
+ target("/metalakes/metalake1/permissions/roles/role1/")
+ .request(MediaType.APPLICATION_JSON_TYPE)
+ .accept("application/vnd.gravitino.v1+json")
+ .put(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE));
+
+ Assertions.assertEquals(
+ Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(),
resp2.getStatus());
+ ErrorResponse errorResponse2 = resp2.readEntity(ErrorResponse.class);
+ Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE,
errorResponse2.getCode());
+ Assertions.assertEquals(RuntimeException.class.getSimpleName(),
errorResponse2.getType());
+ }
}