jerryshao commented on code in PR #13699:
URL: https://github.com/apache/gravitino/pull/13699#discussion_r4228006823


##########
server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizationLookups.java:
##########
@@ -45,18 +45,17 @@ public class TestJcasbinAuthorizationLookups {
 
   @Test
   void testResolveMetadataIdUsesAtomicSharedCacheAndRequestDedup() {
-    MetadataObject table =
-        MetadataObjects.of(Arrays.asList("cat1", "sch1", "tbl1"), 
MetadataObject.Type.TABLE);
+    MetadataObject catalog = MetadataObjects.of(Arrays.asList("cat1"), 
MetadataObject.Type.CATALOG);

Review Comment:
   [Nit] Switching this fixture from `TABLE` to `CATALOG` removes normalization 
from the one test that asserts shared-cache atomicity and request dedup.
   
   `CATALOG` is not in `METADATA_SCOPE_MAPPING`, so `normalizeMetadataObject` 
returns at its `scope == null` early exit and the test never reaches a 
capability lookup — which is presumably why the change was made, since this 
test has no `CatalogManager` mock. The effect is that `getCount == 1` / 
`getIfPresentCount == 0` is now only proven for a type that bypasses the new 
code. The realistic shape (a `TABLE` whose raw and canonical keys differ, where 
one must confirm the shared cache is still consulted exactly once under the 
canonical key) is no longer covered here.
   
   `TestJcasbinMetadataIdCacheNormalization` covers normalization and 
`testLoaderUsesTheCanonicalNameOfItsCacheKey` covers the loader input, so the 
gap is narrow — but keeping a `TABLE` case with the same `CatalogManager` stub 
the new suite already builds would restore it cheaply.
   
   Verified by: reading this test before and after via `git diff 
origin/main...HEAD` on this file, and confirming `CATALOG` is absent from 
`METADATA_SCOPE_MAPPING` (`MetadataIdConverter.java:50-58`) so 
`normalizeMetadataObject` returns at line 117-119.



##########
server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizationLookups.java:
##########
@@ -74,34 +67,28 @@ public JcasbinAuthorizationLookups(
    * Optional#empty()} when the metadata object does not exist so callers can 
deny authorization.
    * Missing metadata objects are never cached as negative results: a later 
create for the same name
    * can be observed without waiting for cache eviction. Existing objects are 
invalidated by local
-   * name-id mapping hooks and by the change-log poller on peer nodes.
+   * name-id mapping hooks and by the change-log poller on peer nodes. Both 
cache tiers use names
+   * normalized by catalog capability. Normalization is deduplicated per raw 
name within a request;
+   * a fresh request still resolves current catalog rules before consulting 
the shared cache.
    */
   public Optional<Long> resolveMetadataId(
       MetadataObject metadataObject, String metalake, 
AuthorizationRequestContext requestContext) {
     try {
-      MetadataObject cacheObject = metadataObject;
-      // Hooks and change-log records use normalized parents. Cache the same 
key for every
-      // equivalent request spelling, while preserving the Gravitino-owned 
model leaf.
-      if (metadataObject.type() == MetadataObject.Type.SEMANTIC_MODEL) {
-        NameIdentifier ident = MetadataObjectUtil.toEntityIdent(metalake, 
metadataObject);
-        Capability capability =
-            CapabilityHelpers.getCapability(ident, 
GravitinoEnv.getInstance().catalogManager());
-        NameIdentifier normalized =
-            NameIdentifier.of(
-                CapabilityHelpers.applyCaseSensitive(
-                    ident.namespace(), Capability.Scope.SEMANTIC_MODEL, 
capability),
-                ident.name());
-        cacheObject =
-            NameIdentifierUtil.toMetadataObject(normalized, 
Entity.EntityType.SEMANTIC_MODEL);
-      }
+      // Use the same capability rules as ID resolution so hooks and peer 
change-log replay
+      // evict every equivalent spelling from both cache tiers.
+      MetadataObject cacheObject =
+          requestContext.computeNormalizedMetadataObjectIfAbsent(
+              JcasbinAuthorizationCacheKeys.metadataIdCacheKey(metalake, 
metadataObject),
+              ignored -> 
MetadataIdConverter.normalizeMetadataObject(metadataObject, metalake));

Review Comment:
   [Important] A capability lookup now runs on every metadata-ID lookup, 
including shared-cache hits, so a catalog whose connector cannot initialize 
will fail authorization requests that previously served from cache.
   
   Not the throughput point Copilot raised in #discussion_r4217217323 
(addressed by the request-local memo) but the failure mode, which memoization 
cannot help: the failure recurs every fresh request.
   
   Chain I traced:
   
   - `CapabilityHelpers.getCapability` 
(`core/src/main/java/org/apache/gravitino/catalog/CapabilityHelpers.java:55-64`)
 rethrows `NoSuchCatalogException` but wraps *every other* `RuntimeException` 
as a bare `RuntimeException("Failed to get capabilities for catalog: ...")`.
   - `resolveMetadataId` only catches `NotFoundException` (line 92 of this 
file). `NoSuchCatalogException extends NotFoundException`; the wrapped one does 
not, so it propagates.
   - `doWithCatalog` leases through `catalogCache.get(ident, 
this::loadCatalogInternal)` (`CatalogManager.java:815-826`, `:1644`), so a 
catalog whose connector constructor fails (metastore unreachable, bad 
credentials) throws on each attempt.
   
   Before this PR, the non-`SEMANTIC_MODEL` path reached 
`metadataIdCache.get(key, loader)` with a raw key and touched `CatalogManager` 
only on a miss, so a long-lived cached ID kept authorizing through a sick 
catalog. Now every call needs the capability first.
   
   Worst case is role policy loading: `resolveRolePolicies` 
(`JcasbinAuthorizer.java:1631-1633`) has an explicit `unresolvedObjects` 
channel so a vanished object degrades gracefully, but this exception bypasses 
it and escapes `loadRole`, which rethrows as `RuntimeException` 
(`AuthorizationRequestContext.java:162-168`). One unhealthy catalog referenced 
by a role therefore aborts the whole request, including objects in healthy 
catalogs.
   
   Suggested fix: treat a non-`NotFoundException` capability failure as a 
normalization failure here and fail closed for that object (`Optional.empty()`) 
instead of letting it escape as a 500. A short-TTL capability cache keyed by 
`(metalake, catalog)` would also cover it, since `Capability` depends on the 
catalog's provider rather than connector liveness.
   
   Verified by: reading `CapabilityHelpers.getCapability` and 
`CatalogManager.doWithCatalog`/`loadCatalogAndWrapInternal` in this checkout; 
`git show origin/main` of this file calls `getCapability` only for 
`SEMANTIC_MODEL`; `TestJcasbinMetadataIdCacheNormalization:392` only exercises 
`NoSuchCatalogException`.



##########
server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizationLookups.java:
##########
@@ -74,34 +67,28 @@ public JcasbinAuthorizationLookups(
    * Optional#empty()} when the metadata object does not exist so callers can 
deny authorization.
    * Missing metadata objects are never cached as negative results: a later 
create for the same name
    * can be observed without waiting for cache eviction. Existing objects are 
invalidated by local
-   * name-id mapping hooks and by the change-log poller on peer nodes.
+   * name-id mapping hooks and by the change-log poller on peer nodes. Both 
cache tiers use names
+   * normalized by catalog capability. Normalization is deduplicated per raw 
name within a request;
+   * a fresh request still resolves current catalog rules before consulting 
the shared cache.
    */
   public Optional<Long> resolveMetadataId(
       MetadataObject metadataObject, String metalake, 
AuthorizationRequestContext requestContext) {
     try {
-      MetadataObject cacheObject = metadataObject;
-      // Hooks and change-log records use normalized parents. Cache the same 
key for every
-      // equivalent request spelling, while preserving the Gravitino-owned 
model leaf.
-      if (metadataObject.type() == MetadataObject.Type.SEMANTIC_MODEL) {
-        NameIdentifier ident = MetadataObjectUtil.toEntityIdent(metalake, 
metadataObject);
-        Capability capability =
-            CapabilityHelpers.getCapability(ident, 
GravitinoEnv.getInstance().catalogManager());
-        NameIdentifier normalized =
-            NameIdentifier.of(
-                CapabilityHelpers.applyCaseSensitive(
-                    ident.namespace(), Capability.Scope.SEMANTIC_MODEL, 
capability),
-                ident.name());
-        cacheObject =
-            NameIdentifierUtil.toMetadataObject(normalized, 
Entity.EntityType.SEMANTIC_MODEL);
-      }
+      // Use the same capability rules as ID resolution so hooks and peer 
change-log replay
+      // evict every equivalent spelling from both cache tiers.
+      MetadataObject cacheObject =
+          requestContext.computeNormalizedMetadataObjectIfAbsent(
+              JcasbinAuthorizationCacheKeys.metadataIdCacheKey(metalake, 
metadataObject),
+              ignored -> 
MetadataIdConverter.normalizeMetadataObject(metadataObject, metalake));
       String cacheKey = 
JcasbinAuthorizationCacheKeys.metadataIdCacheKey(metalake, cacheObject);
       // 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
-      // only positive results, never a negative one.
+      // only positive results, never a negative one. Load the same canonical 
name as the key,
+      // rather than applying the request spelling again in the loader.
       return Optional.of(
           requestContext.computeMetadataIdIfAbsent(
               cacheKey,
-              k -> metadataIdCache.get(k, ignored -> 
loadMetadataId(metadataObject, metalake))));
+              k -> metadataIdCache.get(k, ignored -> 
loadMetadataId(cacheObject, metalake))));

Review Comment:
   [Nit] On a shared-cache miss the capability is resolved twice for the same 
object.
   
   `cacheObject` is already canonical, but `loadMetadataId` calls 
`MetadataIdConverter.getID`, which runs `normalizeCaseSensitive(ident, scope, 
catalogManager)` again (`MetadataIdConverter.java:79-80`) and so issues a 
second `getCapability` -> `doWithCatalog` round trip. Normalizing an 
already-canonical name is idempotent, so this is correctness-neutral; it just 
doubles the catalog work on the miss path, which is the path the new comment 
above is trying to tighten.
   
   If worth tidying, an overload of `getID` that takes a pre-normalized 
identifier (or a package-private `getIdForNormalizedObject`) would skip it. Not 
blocking.
   
   Verified by: reading `getID` lines 69-100 of `MetadataIdConverter.java` in 
this checkout and following `normalizeCaseSensitive` -> `normalizeIdentifier` 
-> `CapabilityHelpers.getCapability`.



##########
server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataIdConverter.java:
##########
@@ -95,14 +99,66 @@ public static Optional<Long> getID(MetadataObject 
metadataObject, String metalak
     return Optional.of(extractIdFromEntity(entity));
   }
 
+  /**
+   * Normalizes a metadata object's name using the same catalog rules as ID 
resolution.
+   *
+   * <p>Types without a catalog capability scope retain their names. Semantic 
model leaves remain
+   * case sensitive, while column names and their table/schema parents follow 
their own scopes.
+   *
+   * @param metadataObject the object whose name will be normalized
+   * @param metalake the metalake name
+   * @return the normalized object, suitable for name-to-ID cache keys
+   * @throws NotFoundException if the containing catalog does not exist
+   */
+  public static MetadataObject normalizeMetadataObject(
+      MetadataObject metadataObject, String metalake) {
+    Preconditions.checkArgument(metadataObject != null, "Metadata object 
cannot be null");
+    Capability.Scope scope = METADATA_SCOPE_MAPPING.get(metadataObject.type());

Review Comment:
   [Question] `METADATA_SCOPE_MAPPING` has no entry for `VIEW` or `FUNCTION`, 
so those two types still key and load on the raw spelling. Is that deliberate 
for this PR?
   
   Both are first-class authorization types, not virtual ones: 
`metadataIdCacheKey` gives `VIEW` and `FUNCTION` their own branch 
(`JcasbinAuthorizationCacheKeys.java:61-68`), 
`MetadataObjectUtil.toEntityIdent` maps both (`:129-140`), `Capability.Scope` 
has both, `hasSchemaParent` returns `true` for both 
(`CapabilityHelpers.java:249-256`), and `VIEW` appears in 
`AuthorizationUtils.SKIP_APPLY_TYPES` (`:102`) only because there is no plugin 
push-down — it is still a securable type. 
`JcasbinChangeListener.VIRTUAL_NAMESPACE_TYPES` (`:70-75`) excludes neither.
   
   With `scope == null` the method returns early, so for a case-insensitive 
catalog a request for `cat.DB.MY_VIEW` keys raw and `getID` loads raw. The 
symptom differs from the `TABLE` bug in #13694: nothing positive gets cached, 
so there is no stale ID — instead the lookup consistently misses the store and 
denies, and the listener's eviction of `cat.db.my_view` never matches the raw 
key.
   
   This predates the PR (`getID` used the same map), so it may well be out of 
scope. Raising it because the PR describes keys as using "the same 
normalization implementation as ID resolution" and the test suite derives its 
type coverage from this map, so the two types that the key factory handles but 
the map omits are invisible to both the claim and the tests. A line in the 
description or a follow-up issue would settle it.
   
   Verified by: reading `METADATA_SCOPE_MAPPING` (lines 50-58) against the 
`VIEW`/`FUNCTION` branches in 
`JcasbinAuthorizationCacheKeys.metadataIdCacheKey`, 
`MetadataObjectUtil.toEntityIdent`, `CapabilityHelpers.hasSchemaParent` and 
`JcasbinChangeListener.VIRTUAL_NAMESPACE_TYPES` in this checkout.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to