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


##########
server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java:
##########
@@ -3814,6 +3820,108 @@ public CapabilityResult caseSensitiveOnName(Scope 
scope) {
     }
   }
 
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  public void 
testMetalakeDropEvictsEveryMetadataTypeAndKeepsOtherMetalake(boolean peerReplay)
+      throws Exception {
+    // Use a dedicated metalake so the shared MetadataIdConverter stubs do not 
leak into other
+    // tests. The other metalake name extends the dropped one to check the 
name boundary.
+    String metalakeName = peerReplay ? "peer_drop_ml" : "local_drop_ml";
+    String otherMetalake = metalakeName + "_other";
+    GravitinoCache<String, Long> metadataIdCache = 
getMetadataIdCache(jcasbinAuthorizer);
+    JcasbinAuthorizationLookups lookups =
+        new JcasbinAuthorizationLookups(metadataIdCache, 
getOwnerRelCache(jcasbinAuthorizer));
+    MetadataObject.Type[] types = MetadataObject.Type.values();
+    for (int i = 0; i < types.length; i++) {
+      MetadataObject object = metadataObjectOfType(metalakeName, types[i]);
+      MetadataObject otherObject = metadataObjectOfType(otherMetalake, 
types[i]);
+      metadataIdConverterMockedStatic
+          .when(() -> MetadataIdConverter.getID(object, metalakeName))
+          .thenReturn(Optional.of(1000L + i), Optional.of(2000L + i));
+      metadataIdConverterMockedStatic
+          .when(() -> MetadataIdConverter.getID(otherObject, otherMetalake))
+          .thenReturn(Optional.of(3000L + i));
+
+      assertEquals(Optional.of(1000L + i), resolve(lookups, object, 
metalakeName));
+      assertEquals(Optional.of(3000L + i), resolve(lookups, otherObject, 
otherMetalake));
+      assertEquals(
+          Optional.of(1000L + i),
+          metadataIdCache.getIfPresent(
+              JcasbinAuthorizationCacheKeys.metadataIdCacheKey(metalakeName, 
object)),
+          types[i].name());
+    }
+
+    metalakeDropInvalidation(metalakeName, peerReplay).run();
+
+    for (int i = 0; i < types.length; i++) {
+      MetadataObject object = metadataObjectOfType(metalakeName, types[i]);
+      MetadataObject otherObject = metadataObjectOfType(otherMetalake, 
types[i]);
+      assertEquals(
+          Optional.empty(),
+          metadataIdCache.getIfPresent(
+              JcasbinAuthorizationCacheKeys.metadataIdCacheKey(metalakeName, 
object)),
+          types[i].name());
+      assertEquals(
+          Optional.of(3000L + i),
+          metadataIdCache.getIfPresent(
+              JcasbinAuthorizationCacheKeys.metadataIdCacheKey(otherMetalake, 
otherObject)),
+          types[i].name());
+      // The recreated metalake resolves same-named objects to their new IDs.
+      assertEquals(Optional.of(2000L + i), resolve(lookups, object, 
metalakeName));
+    }
+  }
+
+  @ParameterizedTest
+  @ValueSource(booleans = {false, true})
+  public void 
testRecreatedMetalakeDoesNotInheritOldOwnershipOrPermissions(boolean peerReplay)
+      throws Exception {
+    // Use a dedicated metalake so the shared mapper and MetadataIdConverter 
stubs do not leak into
+    // other tests.
+    String metalakeName = peerReplay ? "peer_recreate_ml" : 
"local_recreate_ml";
+    String roleName = "recreated_role";
+    long oldCatalogId = 7001L;
+    long oldUserId = 7002L;
+    long oldRoleId = 7003L;
+    long newCatalogId = 8001L;
+    long newUserId = 8002L;
+    long newRoleId = 8003L;
+    long otherCatalogId = 8004L;
+    long otherOwnerId = 9999L;
+    Principal principal = PrincipalUtils.getCurrentPrincipal();
+    MetadataObject catalog = MetadataObjects.parse("cat", 
MetadataObject.Type.CATALOG);
+    MetadataObject otherCatalog = MetadataObjects.parse("other_cat", 
MetadataObject.Type.CATALOG);
+    metadataIdConverterMockedStatic
+        .when(() -> MetadataIdConverter.getID(catalog, metalakeName))
+        .thenReturn(Optional.of(oldCatalogId), Optional.of(newCatalogId));
+    metadataIdConverterMockedStatic
+        .when(() -> MetadataIdConverter.getID(otherCatalog, metalakeName))
+        .thenReturn(Optional.of(otherCatalogId));
+    
when(ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(eq(oldCatalogId), 
eq("CATALOG")))
+        .thenReturn(new OwnerInfo(oldUserId, "USER"));
+    
when(ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(eq(newCatalogId), 
eq("CATALOG")))
+        .thenReturn(new OwnerInfo(otherOwnerId, "USER"));
+    
when(ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(eq(otherCatalogId), 
eq("CATALOG")))
+        .thenReturn(new OwnerInfo(newUserId, "USER"));
+
+    // Old metalake: the user owns "cat" and holds a role that grants 
USE_CATALOG on it.
+    mockMetalakeUserWithRole(metalakeName, oldUserId, oldRoleId, roleName, 
"cat");
+    assertTrue(authorizeUseCatalog(principal, metalakeName, catalog));
+    assertTrue(isOwner(principal, metalakeName, catalog));
+
+    // Drop the metalake, then recreate the same user, role and catalog names 
with new IDs. The
+    // new role grants USE_CATALOG on "other_cat" instead, and someone else 
owns "cat".
+    metalakeDropInvalidation(metalakeName, peerReplay).run();
+    mockedRoleVersions.remove(oldRoleId);
+    mockMetalakeUserWithRole(metalakeName, newUserId, newRoleId, roleName, 
"other_cat");

Review Comment:
   [Nit] This recreate test only exercises the user -> role path. 
`mockMetalakeUserWithRole` (line 4692) stubs `userMetaMapper.getUserUpdatedAt` 
and `roleMetaMapper.listRolesByUserId`, so the symmetric group path is never 
touched, even though the review request that prompted this test named the 
group-role cache as well.
   
   `loadGroupRoles` has its own guard — `cached.getGroupId() == groupId && 
cached.getUpdatedAt() >= groupInfo.getUpdatedAt()` 
(server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java:1340-1347)
 — which is the exact thing this test is meant to prove still holds after a 
metalake is recreated with new entity IDs. Warming `groupRoleCache` with a 
group-bound role and re-mocking the group with a new `groupId` after the drop 
would close the gap cheaply, and would catch a future regression in the group 
guard that the user guard would hide.
   
   Verified by: reading `loadUserRoles` (1285-1307) and `loadGroupRoles` 
(1331-1356) side by side, and the new helper's stubs at 4692-4716 — no 
`GroupMetaMapper.getGroupUpdatedAt` or `listRolesByGroupId` stubbing anywhere 
in the added test code.



##########
core/src/main/java/org/apache/gravitino/hook/MetalakeHookDispatcher.java:
##########
@@ -105,7 +105,11 @@ public boolean dropMetalake(NameIdentifier ident, boolean 
force)
       throws NonEmptyEntityException, MetalakeInUseException {
     // Child catalog/schema/fileset write-through secrets are cleaned when 
MetalakeManager
     // force-drops catalogs via CatalogManager.dropCatalog → 
FilesetCatalogOperations.dropSchema.
-    return dispatcher.dropMetalake(ident, force);
+    boolean dropped = dispatcher.dropMetalake(ident, force);
+    if (dropped) {

Review Comment:
   [Question] This is the one case where the node can keep stale entries: 
`MetalakeManager.dropMetalake` maps a concurrent removal to `false` rather than 
an exception 
(core/src/main/java/org/apache/gravitino/metalake/MetalakeManager.java:409-412, 
`NoSuchMetalakeException | NoSuchEntityException` -> `return false`), and 
`store.delete` also returns `false` when the row is already gone. In that path 
this node skips the eviction while its `metadataIdCache` may still hold the 
dropped metalake's prefix.
   
   It does self-heal: the peer that won the delete wrote a METALAKE DROP row, 
and this node's `JcasbinChangeListener` replays it within 
`gravitino.authorization.jcasbin.changePollIntervalSecs` (default 3s), 
re-entering the same prefix invalidation. So this is a question, not a blocker 
— but since a `metadataIdCache` entry is always re-loadable, invalidating 
regardless of the return value costs nothing and removes the window. Was the 
guard deliberate (keeping the hook a no-op when this node changed nothing)? If 
so, leaving it as is seems fine to me.
   
   Verified by: reading `MetalakeManager.dropMetalake` (lines 369-424) for 
every path that returns `false`, and confirming `selectEntityChanges` applies 
no entity-type filter (EntityChangeLogBaseSQLProvider.java:52-59) so the peer's 
METALAKE row is in fact replayed here.



-- 
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