Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
6a44057
[#10772] feat(authz): Eventual-consistency invalidation for the Jcasb…
yuqi1129 May 15, 2026
62ae012
[#10772] feat(authz): Version-validated role caching for JcasbinAutho…
yuqi1129 May 15, 2026
d985bfb
fix(authz): address jcasbin cache review comments
yuqi1129 May 15, 2026
7d30c8f
test(authz): stabilize jcasbin poller tests
yuqi1129 May 15, 2026
7a686f1
docs(authz): document jcasbin cache settings
yuqi1129 May 15, 2026
e74c005
Fix atomic authorization cache loading
yuqi1129 May 18, 2026
7301353
Address jcasbin cache invalidation review comments
yuqi1129 May 18, 2026
3718d64
fix
yuqi1129 May 18, 2026
3deda80
fix
yuqi1129 May 18, 2026
63d331c
Merge branch 'main' into feat/jcasbin-eventual-consistency
yuqi1129 May 18, 2026
07c0ce0
Fix owner cache invalidation on initial owner set
yuqi1129 May 18, 2026
7229cf6
Merge branch 'feat/jcasbin-eventual-consistency' of github.com:yuqi11…
yuqi1129 May 18, 2026
4e2d2a3
Synchronize jcasbin change poll cursor updates
yuqi1129 May 18, 2026
1a6e163
Potential fix for pull request finding
yuqi1129 May 19, 2026
9054927
fix
yuqi1129 May 19, 2026
113a5e4
Isolate GravitinoEnv state in AuthorizationUtils notify tests
yuqi1129 May 19, 2026
540fec7
Align AuthorizationUtils notify tests with conditional rename branch
yuqi1129 May 19, 2026
1566925
Serialize cache reads against invalidations with a fair RW lock
yuqi1129 May 19, 2026
a42462a
Document defensive synchronized on jcasbin poll methods
yuqi1129 May 19, 2026
6833ff3
Address Copilot review on poller shutdown and owner notify
yuqi1129 May 19, 2026
fd98d97
Merge branch 'feat/jcasbin-eventual-consistency' into feat/jcasbin-ca…
yuqi1129 May 20, 2026
c75338d
Merge origin/main into feat/jcasbin-cache-refactor
yuqi1129 May 20, 2026
074baad
refactor(auth): centralize jcasbin cache keys
yuqi1129 May 20, 2026
17cada9
fix(auth): remove unused server-common test lombok
yuqi1129 May 20, 2026
166b3fd
fix(auth): refresh jcasbin cached role policies safely
yuqi1129 May 20, 2026
c6e5b0a
Merge remote-tracking branch 'origin/main' into feat/jcasbin-cache-re…
yuqi1129 May 20, 2026
31b6859
fix(auth): validate group id for jcasbin group role cache
yuqi1129 May 20, 2026
b834408
Fix JCasbin user role cache validation
yuqi1129 May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion core/src/main/java/org/apache/gravitino/Configs.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,10 @@ private Configs() {}

public static final ConfigEntry<Long> GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE =
new ConfigBuilder("gravitino.authorization.jcasbin.roleCacheSize")
.doc("The maximum size of the role cache for authorization")
.doc(
"The maximum size of the role-related caches used by the JcasbinAuthorizer. "
+ "Shared by the user-role, group-role, and loaded-role caches, so the "
+ "effective memory footprint is up to roughly 3x this value.")
.version(ConfigConstants.VERSION_1_1_1)
.longConf()
.createWithDefault(DEFAULT_GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.server.authorization.jcasbin;

import java.util.List;

/**
* Cached snapshot of a group's role assignments. The {@code updatedAt} timestamp corresponds to the
* {@code group_meta.updated_at} column and is used as a version sentinel: if the DB value is newer,
* the cached role list is stale and must be reloaded.
*/
final class CachedGroupRoles {

private final long groupId;
private final long updatedAt;
private final List<Long> roleIds;

CachedGroupRoles(long groupId, long updatedAt, List<Long> roleIds) {
this.groupId = groupId;
this.updatedAt = updatedAt;
this.roleIds = roleIds;
}

long getGroupId() {
return groupId;
}

long getUpdatedAt() {
return updatedAt;
}

List<Long> getRoleIds() {
return roleIds;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* 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.server.authorization.jcasbin;

import java.util.List;

/**
* Cached snapshot of a user's direct role assignments. The {@code updatedAt} timestamp corresponds
* to the {@code user_meta.updated_at} column and is used as a version sentinel: if the DB value is
* newer, the cached role list is stale and must be reloaded.
*/
final class CachedUserRoles {

private final long userId;
private final long updatedAt;
private final List<Long> roleIds;

CachedUserRoles(long userId, long updatedAt, List<Long> roleIds) {
this.userId = userId;
this.updatedAt = updatedAt;
this.roleIds = roleIds;
}

long getUserId() {
return userId;
}

long getUpdatedAt() {
return updatedAt;
}

List<Long> getRoleIds() {
return roleIds;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.server.authorization.jcasbin;

import com.google.common.annotations.VisibleForTesting;
import org.apache.gravitino.MetadataObject;

/** Cache key factory for JCasbin authorization caches. */
final class JcasbinAuthorizationCacheKeys {

/** Unit Separator for internal cache keys. */
static final String SEPARATOR = "\u001F";

private JcasbinAuthorizationCacheKeys() {}

/**
* Builds a path-based key for the metadata id cache.
*
* <p>Container objects end with the internal separator so prefix invalidation can remove both the
* container and entries under the same name path. Leaf objects include the type suffix to avoid
* collisions between objects that share the same name path.
*/
static String metadataObjectKey(String metalake, MetadataObject metadataObject) {
if (metadataObject.type() == MetadataObject.Type.METALAKE) {
return metalake + SEPARATOR;
}

StringBuilder sb = new StringBuilder(metalake);
sb.append(SEPARATOR);
sb.append(String.join(SEPARATOR, metadataObject.fullName().split("\\.")));
if (isMetadataContainer(metadataObject.type())) {
sb.append(SEPARATOR);
} else {
sb.append(SEPARATOR);
sb.append(metadataObject.type().name());
}
return sb.toString();
}

static String userRoleKey(String metalake, String username) {
return "USER" + SEPARATOR + metalake + SEPARATOR + username;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we use metalake + SEPARATOR + USER_ROLE_REL SEPARATOR username?

}

static String groupRoleKey(String metalake, String groupname) {
return "GROUP" + SEPARATOR + metalake + SEPARATOR + groupname;
}

@VisibleForTesting
static boolean isMetadataContainer(MetadataObject.Type type) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could u put this into MetadataObjectUtil? We should have a unified place to define this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we should have a better name. Because table can contain column.

return type == MetadataObject.Type.METALAKE
|| type == MetadataObject.Type.CATALOG
|| type == MetadataObject.Type.SCHEMA;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
package org.apache.gravitino.server.authorization.jcasbin;

import com.google.common.annotations.VisibleForTesting;
import java.util.Optional;
import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.authorization.AuthorizationRequestContext;
Expand All @@ -43,17 +42,14 @@
*/
public class JcasbinAuthorizationLookups {

/** Unit Separator for internal path-based cache keys. */
static final String KEY_SEP = "\u001F";

private final GravitinoCache<String, Long> metadataIdCache;
private final GravitinoCache<Long, Optional<OwnerInfo>> ownerRelCache;

/**
* Creates a new lookups facade around the supplied caches. The caches are owned by the caller and
* remain accessible for invalidation by other components (poller, change hooks).
*
* @param metadataIdCache path-based {@code metalake::catalog::schema::object::TYPE} → entity id
* @param metadataIdCache path-based metadata object key → entity id
* @param ownerRelCache {@code metadataObjectId} → {@link Optional} of {@link OwnerInfo}
*/
public JcasbinAuthorizationLookups(
Expand All @@ -72,7 +68,7 @@ public JcasbinAuthorizationLookups(
*/
public Optional<Long> resolveMetadataId(
MetadataObject metadataObject, String metalake, AuthorizationRequestContext requestContext) {
String cacheKey = buildCacheKey(metalake, metadataObject);
String cacheKey = JcasbinAuthorizationCacheKeys.metadataObjectKey(metalake, metadataObject);
try {
// Both cache tiers load atomically and forbid caching null, so a missing object is signalled
// by throwing through the loaders and translated back to Optional.empty() here. This caches
Expand Down Expand Up @@ -116,52 +112,4 @@ public Optional<OwnerInfo> resolveOwnerId(
return ownerInfo == null ? Optional.empty() : Optional.of(ownerInfo);
}));
}

/** Underlying metadata-id cache; exposed for invalidation by the change hooks and the poller. */
public GravitinoCache<String, Long> metadataIdCache() {
return metadataIdCache;
}

/** Underlying owner cache; exposed for invalidation by the change hooks and the poller. */
public GravitinoCache<Long, Optional<OwnerInfo>> ownerRelCache() {
return ownerRelCache;
}

/**
* Builds a path-based cache key for the metadataIdCache. Container objects end with the internal
* separator so a prefix invalidation can remove the container and all entries under the same name
* path.
*
* <p>Examples: {@code metalake<sep>}, {@code metalake<sep>catalog<sep>}, {@code
* metalake<sep>catalog<sep>schema<sep>}, {@code
* metalake<sep>catalog<sep>schema<sep>table<sep>TABLE}.
*/
@VisibleForTesting
public static String buildCacheKey(String metalake, MetadataObject metadataObject) {
if (metadataObject.type() == MetadataObject.Type.METALAKE) {
return metalake + KEY_SEP;
}
StringBuilder sb = new StringBuilder(metalake);
sb.append(KEY_SEP);
// fullName uses '.' as separator, e.g. "catalog1.schema1.table1"
String[] parts = metadataObject.fullName().split("\\.");
sb.append(String.join(KEY_SEP, parts));
if (isContainerType(metadataObject.type())) {
// Trailing separator enables prefix-based invalidation.
sb.append(KEY_SEP);
} else {
// Leaf nodes get the type suffix to avoid collisions
sb.append(KEY_SEP);
sb.append(metadataObject.type().name());
}
return sb.toString();
}

/** Returns true for entity types that can contain children (metalake, catalog, schema). */
@VisibleForTesting
public static boolean isContainerType(MetadataObject.Type type) {
return type == MetadataObject.Type.METALAKE
|| type == MetadataObject.Type.CATALOG
|| type == MetadataObject.Type.SCHEMA;
}
}
Loading
Loading