This is an automated email from the ASF dual-hosted git repository.
yuqi4733 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 2ac779ca81 Revert "[#10117] improvement(authz): Remove ownership
relation cache" (#10279)
2ac779ca81 is described below
commit 2ac779ca81f47ff76dc182db7b934fc4a7583611
Author: Qi Yu <[email protected]>
AuthorDate: Fri Mar 6 16:50:50 2026 +0800
Revert "[#10117] improvement(authz): Remove ownership relation cache"
(#10279)
Reverts apache/gravitino#10119
---
.../main/java/org/apache/gravitino/Configs.java | 9 ++++
.../authorization/GravitinoAuthorizer.java | 12 +++++
.../gravitino/authorization/OwnerManager.java | 30 ++++++++++++
docs/security/access-control.md | 7 ++-
.../authorization/PassThroughAuthorizer.java | 4 ++
.../authorization/jcasbin/JcasbinAuthorizer.java | 55 ++++++++++++++++------
.../authorization/MockGravitinoAuthorizer.java | 4 ++
.../jcasbin/TestJcasbinAuthorizer.java | 44 +++++++++++++++++
.../filter/TestGravitinoInterceptionService.java | 4 ++
9 files changed, 153 insertions(+), 16 deletions(-)
diff --git a/core/src/main/java/org/apache/gravitino/Configs.java
b/core/src/main/java/org/apache/gravitino/Configs.java
index a26f3c36a5..3cc9d703e6 100644
--- a/core/src/main/java/org/apache/gravitino/Configs.java
+++ b/core/src/main/java/org/apache/gravitino/Configs.java
@@ -320,6 +320,15 @@ public class Configs {
.longConf()
.createWithDefault(DEFAULT_GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE);
+ public static final long DEFAULT_GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE =
100000L;
+
+ public static final ConfigEntry<Long>
GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE =
+ new ConfigBuilder("gravitino.authorization.jcasbin.ownerCacheSize")
+ .doc("The maximum size of the owner cache for authorization")
+ .version(ConfigConstants.VERSION_1_1_1)
+ .longConf()
+ .createWithDefault(DEFAULT_GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE);
+
public static final ConfigEntry<List<String>> SERVICE_ADMINS =
new ConfigBuilder("gravitino.authorization.serviceAdmins")
.doc("The admins of Gravitino service")
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
b/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
index 3a4fbfc0a9..7965f173ad 100644
---
a/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
+++
b/core/src/main/java/org/apache/gravitino/authorization/GravitinoAuthorizer.java
@@ -152,4 +152,16 @@ public interface GravitinoAuthorizer extends Closeable {
throw new RuntimeException("Can not get Role Entity", e);
}
}
+
+ /**
+ * This method is called to clear the owner relationship in jcasbin when the
owner of the metadata
+ * changes.
+ *
+ * @param metalake metalake;
+ * @param oldOwnerId The old owner id;
+ * @param nameIdentifier The metadata name identifier;
+ * @param type entity type
+ */
+ void handleMetadataOwnerChange(
+ String metalake, Long oldOwnerId, NameIdentifier nameIdentifier,
Entity.EntityType type);
}
diff --git
a/core/src/main/java/org/apache/gravitino/authorization/OwnerManager.java
b/core/src/main/java/org/apache/gravitino/authorization/OwnerManager.java
index 13fa04a80d..5b7ce28c98 100644
--- a/core/src/main/java/org/apache/gravitino/authorization/OwnerManager.java
+++ b/core/src/main/java/org/apache/gravitino/authorization/OwnerManager.java
@@ -25,6 +25,7 @@ import java.util.Optional;
import lombok.Getter;
import org.apache.gravitino.Entity;
import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.NameIdentifier;
import org.apache.gravitino.SupportsRelationOperations;
@@ -36,6 +37,7 @@ import org.apache.gravitino.lock.TreeLockUtils;
import org.apache.gravitino.meta.GroupEntity;
import org.apache.gravitino.meta.UserEntity;
import org.apache.gravitino.utils.MetadataObjectUtil;
+import org.apache.gravitino.utils.NameIdentifierUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -118,6 +120,7 @@ public class OwnerManager implements OwnerDispatcher {
metadataObject,
authorizationPlugin ->
authorizationPlugin.onOwnerSet(metadataObject,
originOwner.orElse(null), newOwner));
+ originOwner.ifPresent(owner -> notifyOwnerChange(owner, metalake,
metadataObject));
} catch (NoSuchEntityException nse) {
LOG.warn(
"Metadata object {} or owner {} is not found",
metadataObject.fullName(), ownerName, nse);
@@ -133,6 +136,33 @@ public class OwnerManager implements OwnerDispatcher {
}
}
+ private void notifyOwnerChange(Owner oldOwner, String metalake,
MetadataObject metadataObject) {
+ GravitinoAuthorizer gravitinoAuthorizer =
GravitinoEnv.getInstance().gravitinoAuthorizer();
+ if (gravitinoAuthorizer != null) {
+ if (oldOwner.type() == Owner.Type.USER) {
+ try {
+ UserEntity userEntity =
+ GravitinoEnv.getInstance()
+ .entityStore()
+ .get(
+ NameIdentifierUtil.ofUser(metalake, oldOwner.name()),
+ Entity.EntityType.USER,
+ UserEntity.class);
+ gravitinoAuthorizer.handleMetadataOwnerChange(
+ metalake,
+ userEntity.id(),
+ MetadataObjectUtil.toEntityIdent(metalake, metadataObject),
+ Entity.EntityType.valueOf(metadataObject.type().name()));
+ } catch (IOException e) {
+ LOG.warn(e.getMessage(), e);
+ }
+ } else {
+ throw new UnsupportedOperationException(
+ "Notification for Group Owner is not supported yet.");
+ }
+ }
+ }
+
@Override
public Optional<Owner> getOwner(String metalake, MetadataObject
metadataObject) {
NameIdentifier ident = MetadataObjectUtil.toEntityIdent(metalake,
metadataObject);
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index bb8e85aee1..1724cdde07 100644
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -446,17 +446,20 @@ To enable access control in Gravitino, configure the
following settings in your
| `gravitino.authorization.serviceAdmins` | Comma-separated
list of service administrator usernames | (none) | Yes
(when authorization is enabled) | 0.5.0 |
| `gravitino.authorization.jcasbin.cacheExpirationSecs` | The expiration
time in seconds for authorization cache entries | `3600` | No
| 1.1.1 |
| `gravitino.authorization.jcasbin.roleCacheSize` | The maximum size
of the role cache for authorization | `10000` | No
| 1.1.1 |
+| `gravitino.authorization.jcasbin.ownerCacheSize` | The maximum size
of the owner cache for authorization | `100000` | No
| 1.1.1 |
### Authorization Cache
-Gravitino uses Caffeine caches to improve authorization performance by caching
role information. The cache configuration options allow you to tune the cache
behavior:
+Gravitino uses Caffeine caches to improve authorization performance by caching
role and owner information. The cache configuration options allow you to tune
the cache behavior:
- **`cacheExpirationSecs`**: Controls how long cache entries remain valid.
After this time, entries are automatically evicted and reloaded from the
backend on the next access. Lower values provide more up-to-date authorization
decisions but may increase load on the backend.
- **`roleCacheSize`**: Controls the maximum number of role entries that can be
cached. When the cache reaches this size, the least recently used entries are
evicted.
+- **`ownerCacheSize`**: Controls the maximum number of owner relationship
entries that can be cached. This cache maps metadata object IDs to their owner
IDs.
+
:::info
-When role privileges are changed through the Gravitino API, the corresponding
cache entries are automatically invalidated to ensure authorization decisions
reflect the latest state.
+When role privileges or ownership are changed through the Gravitino API, the
corresponding cache entries are automatically invalidated to ensure
authorization decisions reflect the latest state.
:::
### Important Notes
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
index 82a5d3a82a..06e20b8ada 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
@@ -109,6 +109,10 @@ public class PassThroughAuthorizer implements
GravitinoAuthorizer {
@Override
public void handleRolePrivilegeChange(String metalake, String roleName) {}
+ @Override
+ public void handleMetadataOwnerChange(
+ String metalake, Long oldOwnerId, NameIdentifier nameIdentifier,
Entity.EntityType type) {}
+
@Override
public void close() throws IOException {}
}
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
index 524c90f032..cea47e353b 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
@@ -29,6 +29,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -86,6 +87,8 @@ public class JcasbinAuthorizer implements GravitinoAuthorizer
{
*/
private Cache<Long, Boolean> loadedRoles;
+ private Cache<Long, Optional<Long>> ownerRel;
+
private Executor executor = null;
@Override
@@ -96,6 +99,8 @@ public class JcasbinAuthorizer implements GravitinoAuthorizer
{
.get(Configs.GRAVITINO_AUTHORIZATION_CACHE_EXPIRATION_SECS);
long roleCacheSize =
GravitinoEnv.getInstance().config().get(Configs.GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE);
+ long ownerCacheSize =
+
GravitinoEnv.getInstance().config().get(Configs.GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE);
// Initialize enforcers before the caches that reference them in removal
listeners
allowEnforcer = new SyncedEnforcer(getModel("/jcasbin_model.conf"), new
GravitinoAdapter());
@@ -116,7 +121,11 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
}
})
.build();
-
+ ownerRel =
+ Caffeine.newBuilder()
+ .expireAfterAccess(cacheExpirationSecs, TimeUnit.SECONDS)
+ .maximumSize(ownerCacheSize)
+ .build();
executor =
Executors.newFixedThreadPool(
GravitinoEnv.getInstance()
@@ -209,9 +218,15 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
String metalake,
MetadataObject metadataObject,
AuthorizationRequestContext requestContext) {
+ Long userId;
boolean result;
try {
- result = checkOwner(metalake, metadataObject, principal.getName());
+ Long metadataId = MetadataIdConverter.getID(metadataObject, metalake);
+ loadOwnerPolicy(metalake, metadataObject, metadataId);
+ UserEntity userEntity = getUserEntity(principal.getName(), metalake);
+ userId = userEntity.id();
+ metadataId = MetadataIdConverter.getID(metadataObject, metalake);
+ result = Objects.equals(Optional.of(userId),
ownerRel.getIfPresent(metadataId));
} catch (Exception e) {
LOG.debug("Can not get entity id", e);
result = false;
@@ -372,6 +387,14 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
loadedRoles.invalidate(roleId);
}
+ @Override
+ public void handleMetadataOwnerChange(
+ String metalake, Long oldOwnerId, NameIdentifier nameIdentifier,
Entity.EntityType type) {
+ MetadataObject metadataObject =
NameIdentifierUtil.toMetadataObject(nameIdentifier, type);
+ Long metadataId = MetadataIdConverter.getID(metadataObject, metalake);
+ ownerRel.invalidate(metadataId);
+ }
+
@Override
public void close() throws IOException {
if (executor != null) {
@@ -408,9 +431,6 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
AuthorizationRequestContext requestContext) {
Long metadataId;
Long userId;
- if (AuthConstants.OWNER.equals(privilege)) {
- return checkOwner(metalake, metadataObject, username);
- }
try {
UserEntity userEntity = getUserEntity(username, metalake);
userId = userEntity.id();
@@ -419,13 +439,16 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
LOG.debug("Can not get entity id", e);
return false;
}
-
loadRolePrivilege(metalake, username, userId, requestContext);
return authorizeByJcasbin(userId, metadataObject, metadataId, privilege);
}
private boolean authorizeByJcasbin(
Long userId, MetadataObject metadataObject, Long metadataId, String
privilege) {
+ if (AuthConstants.OWNER.equals(privilege)) {
+ Optional<Long> owner = ownerRel.getIfPresent(metadataId);
+ return Objects.equals(Optional.of(userId), owner);
+ }
return enforcer.enforce(
String.valueOf(userId),
String.valueOf(metadataObject.type()),
@@ -495,7 +518,11 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
});
}
- private boolean checkOwner(String metalake, MetadataObject metadataObject,
String userName) {
+ private void loadOwnerPolicy(String metalake, MetadataObject metadataObject,
Long metadataId) {
+ if (ownerRel.getIfPresent(metadataId) != null) {
+ LOG.debug("Metadata {} OWNER has been loaded.", metadataId);
+ return;
+ }
try {
NameIdentifier entityIdent = MetadataObjectUtil.toEntityIdent(metalake,
metadataObject);
EntityStore entityStore = GravitinoEnv.getInstance().entityStore();
@@ -506,19 +533,19 @@ public class JcasbinAuthorizer implements
GravitinoAuthorizer {
SupportsRelationOperations.Type.OWNER_REL,
entityIdent,
Entity.EntityType.valueOf(metadataObject.type().name()));
-
- for (Entity ownerEntity : owners) {
- if (ownerEntity instanceof UserEntity) {
- UserEntity user = (UserEntity) ownerEntity;
- if (user.name().equals(userName)) {
- return true;
+ if (owners.isEmpty()) {
+ ownerRel.put(metadataId, Optional.empty());
+ } else {
+ for (Entity ownerEntity : owners) {
+ if (ownerEntity instanceof UserEntity) {
+ UserEntity user = (UserEntity) ownerEntity;
+ ownerRel.put(metadataId, Optional.of(user.id()));
}
}
}
} catch (IOException e) {
LOG.warn("Can not load metadata owner", e);
}
- return false;
}
private void loadPolicyByRoleEntity(RoleEntity roleEntity) {
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
index acb388c689..f9ef64bab0 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/MockGravitinoAuthorizer.java
@@ -112,6 +112,10 @@ public class MockGravitinoAuthorizer implements
GravitinoAuthorizer {
@Override
public void handleRolePrivilegeChange(Long roleId) {}
+ @Override
+ public void handleMetadataOwnerChange(
+ String metalake, Long oldOwnerId, NameIdentifier nameIdentifier,
Entity.EntityType type) {}
+
@Override
public void close() {}
}
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
index 41088df93c..4388ef0952 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
@@ -36,7 +36,9 @@ import com.google.common.collect.ImmutableList;
import java.io.IOException;
import java.lang.reflect.Field;
import java.security.Principal;
+import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;
import org.apache.commons.lang3.reflect.FieldUtils;
@@ -243,7 +245,17 @@ public class TestJcasbinAuthorizer {
eq(SupportsRelationOperations.Type.OWNER_REL),
eq(catalogIdent),
eq(Entity.EntityType.CATALOG));
+ getOwnerRelCache(jcasbinAuthorizer).invalidateAll();
assertTrue(doAuthorizeOwner(currentPrincipal));
+ doReturn(new ArrayList<>())
+ .when(supportsRelationOperations)
+ .listEntitiesByRelation(
+ eq(SupportsRelationOperations.Type.OWNER_REL),
+ eq(catalogIdent),
+ eq(Entity.EntityType.CATALOG));
+ jcasbinAuthorizer.handleMetadataOwnerChange(
+ METALAKE, USER_ID, catalogIdent, Entity.EntityType.CATALOG);
+ assertFalse(doAuthorizeOwner(currentPrincipal));
}
private Boolean doAuthorize(Principal currentPrincipal) {
@@ -367,6 +379,28 @@ public class TestJcasbinAuthorizer {
assertNull(loadedRoles.getIfPresent(testRoleId));
}
+ @Test
+ public void testOwnerCacheInvalidation() throws Exception {
+ // Get the ownerRel cache via reflection
+ Cache<Long, Optional<Long>> ownerRel = getOwnerRelCache(jcasbinAuthorizer);
+
+ // Manually add an owner relation to the cache
+ ownerRel.put(CATALOG_ID, Optional.of(USER_ID));
+
+ // Verify it's in the cache
+ assertNotNull(ownerRel.getIfPresent(CATALOG_ID));
+
+ // Create a mock NameIdentifier for the metadata object
+ NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(METALAKE,
"testCatalog");
+
+ // Call handleMetadataOwnerChange which should invalidate the cache entry
+ jcasbinAuthorizer.handleMetadataOwnerChange(
+ METALAKE, USER_ID, catalogIdent, Entity.EntityType.CATALOG);
+
+ // Verify it's removed from the cache
+ assertNull(ownerRel.getIfPresent(CATALOG_ID));
+ }
+
@Test
public void testRoleCacheSynchronousRemovalListenerDeletesPolicy() throws
Exception {
makeCompletableFutureUseCurrentThread(jcasbinAuthorizer);
@@ -406,8 +440,10 @@ public class TestJcasbinAuthorizer {
public void testCacheInitialization() throws Exception {
// Verify that caches are initialized
Cache<Long, Boolean> loadedRoles = getLoadedRolesCache(jcasbinAuthorizer);
+ Cache<Long, Optional<Long>> ownerRel = getOwnerRelCache(jcasbinAuthorizer);
assertNotNull(loadedRoles, "loadedRoles cache should be initialized");
+ assertNotNull(ownerRel, "ownerRel cache should be initialized");
}
@SuppressWarnings("unchecked")
@@ -418,6 +454,14 @@ public class TestJcasbinAuthorizer {
return (Cache<Long, Boolean>) field.get(authorizer);
}
+ @SuppressWarnings("unchecked")
+ private static Cache<Long, Optional<Long>>
getOwnerRelCache(JcasbinAuthorizer authorizer)
+ throws Exception {
+ Field field = JcasbinAuthorizer.class.getDeclaredField("ownerRel");
+ field.setAccessible(true);
+ return (Cache<Long, Optional<Long>>) field.get(authorizer);
+ }
+
private static Enforcer getAllowEnforcer(JcasbinAuthorizer authorizer)
throws Exception {
Field field = JcasbinAuthorizer.class.getDeclaredField("allowEnforcer");
field.setAccessible(true);
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
index 7a7cbad24a..a96160da84 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
@@ -337,6 +337,10 @@ public class TestGravitinoInterceptionService {
@Override
public void handleRolePrivilegeChange(Long roleId) {}
+ @Override
+ public void handleMetadataOwnerChange(
+ String metalake, Long oldOwnerId, NameIdentifier nameIdentifier,
Entity.EntityType type) {}
+
@Override
public void close() throws IOException {}
}