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


##########
server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java:
##########
@@ -491,52 +491,40 @@ public void testDottedMetadataNameReturnsBadRequest() 
throws Throwable {
   }
 
   @Test
-  public void testMetalakeNotExist() throws Throwable {
+  public void testMissingAndInaccessibleMetalakeHaveSameResponse() throws 
Throwable {
     try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
-        MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
-            mockStatic(GravitinoAuthorizerProvider.class);
         MockedStatic<AuthorizationUtils> authorizationUtilsMocked =
             mockStatic(AuthorizationUtils.class)) {
-
-      principalUtilsMocked
-          .when(PrincipalUtils::getCurrentPrincipal)
-          .thenReturn(new UserPrincipal("tester"));
       
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
-
-      MethodInvocation methodInvocation = mock(MethodInvocation.class);
-      GravitinoAuthorizerProvider mockedProvider = 
mock(GravitinoAuthorizerProvider.class);
-      
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
-      when(mockedProvider.getGravitinoAuthorizer()).thenReturn(new 
MockGravitinoAuthorizer());
-
-      // Mock AuthorizationUtils.checkCurrentUser to throw 
NoSuchMetalakeException
       authorizationUtilsMocked
           .when(
               () ->
                   AuthorizationUtils.checkCurrentUser(
                       ArgumentMatchers.any(), ArgumentMatchers.any(), 
ArgumentMatchers.any()))
-          .thenThrow(new NoSuchMetalakeException("Metalake nonExistentMetalake 
does not exist"));
+          .thenThrow(new NoSuchMetalakeException("Metalake target does not 
exist"))
+          .thenThrow(new ForbiddenException("User is not a member of target"));

Review Comment:
   [Nit] Only the `dynamicMetalake = false` path is covered here.
   
   The two stubs replay `NoSuchMetalakeException` then `ForbiddenException` 
through `TestOperations.testMethod`, which has an 
`@AuthorizationMetadata(METALAKE)` parameter (line 1034-1036) and therefore 
always takes the non-dynamic branch. The dynamic branch is the one where the 
two responses still differ (see the comment on 
GravitinoInterceptionService.java:307-309), so it is the one most worth pinning 
once that is settled.
   
   A third case using an operation without a METALAKE-annotated parameter — so 
`metalakeIdent` is null and `getAuthorizationMetalake()` supplies the name — 
would assert whatever behavior you decide on there, and would fail if someone 
later re-introduces a distinguishing response on either path.
   
   Verified by: read the test at lines 493-528 and `TestOperations` at lines 
1029-1038; read the two call sites of `validateCurrentUserAndActiveRoles` at 
GravitinoInterceptionService.java:180-187 and 224-235 to confirm which one this 
test reaches.



##########
server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java:
##########
@@ -361,6 +350,14 @@ private Optional<Response> 
validateCurrentUserAndActiveRoles(
       return Optional.empty();
     }
 
+    private Response metalakeMembershipFailure(String user, String metalake) {
+      return Utils.forbidden(
+          String.format(
+              "Current user %s is not a member of metalake %s, or the metalake 
does not exist",
+              user, metalake),
+          null);

Review Comment:
   [Question] Is the Iceberg/Lance REST surface meant to be in scope? It still 
returns the old message this issue is about.
   
   `BaseMetadataAuthorizationMethodInterceptor` runs the identical membership 
gate but was not touched: it calls the same 
`AuthorizationUtils.checkCurrentUser` and rethrows the `ForbiddenException` 
unchanged 
(server-common/.../BaseMetadataAuthorizationMethodInterceptor.java:243-252), so 
callers there still get `Current user %s doesn't exist in the metalake %s, you 
should add the user to the metalake first` (AuthorizationUtils.java:131-133) — 
the wording #13360 calls out as telling callers to add a user to a metalake 
that may not exist. It is used by 
`IcebergMetadataAuthorizationMethodInterceptor` and 
`LanceMetadataAuthorizationMethodInterceptor`, both on live REST paths.
   
   There is no existence oracle on that path under JCasbin (`isMetalakeUser` 
returns false for a missing metalake, so both cases land on the same 
`ForbiddenException`), so this is not the same severity as the lineage gap — 
but the misleading message survives, and 
`TrinoIcebergRESTAuthorizationIT.java:44` notes Trino surfaces that message 
verbatim to end users. If the intent is to fix this at the source, moving the 
neutral wording into `AuthorizationUtils.checkCurrentUser` would cover both 
interceptors and make `metalakeMembershipFailure` unnecessary. If it is 
deliberately out of scope, a line in the PR description saying so would help.
   
   Verified by: read BaseMetadataAuthorizationMethodInterceptor.java:225-320; 
`grep -rn "checkCurrentUser(" --include=*.java` outside tests returns exactly 
two call sites, this file and GravitinoInterceptionService.java:296; `grep -rn 
"extends BaseMetadataAuthorizationMethodInterceptor"` returns the Iceberg and 
Lance interceptors.



##########
server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java:
##########
@@ -491,52 +491,40 @@ public void testDottedMetadataNameReturnsBadRequest() 
throws Throwable {
   }
 
   @Test
-  public void testMetalakeNotExist() throws Throwable {
+  public void testMissingAndInaccessibleMetalakeHaveSameResponse() throws 
Throwable {
     try (MockedStatic<PrincipalUtils> principalUtilsMocked = 
mockStatic(PrincipalUtils.class);
-        MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
-            mockStatic(GravitinoAuthorizerProvider.class);
         MockedStatic<AuthorizationUtils> authorizationUtilsMocked =
             mockStatic(AuthorizationUtils.class)) {
-
-      principalUtilsMocked
-          .when(PrincipalUtils::getCurrentPrincipal)
-          .thenReturn(new UserPrincipal("tester"));
       
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
-
-      MethodInvocation methodInvocation = mock(MethodInvocation.class);
-      GravitinoAuthorizerProvider mockedProvider = 
mock(GravitinoAuthorizerProvider.class);
-      
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
-      when(mockedProvider.getGravitinoAuthorizer()).thenReturn(new 
MockGravitinoAuthorizer());
-
-      // Mock AuthorizationUtils.checkCurrentUser to throw 
NoSuchMetalakeException
       authorizationUtilsMocked
           .when(
               () ->
                   AuthorizationUtils.checkCurrentUser(
                       ArgumentMatchers.any(), ArgumentMatchers.any(), 
ArgumentMatchers.any()))
-          .thenThrow(new NoSuchMetalakeException("Metalake nonExistentMetalake 
does not exist"));
+          .thenThrow(new NoSuchMetalakeException("Metalake target does not 
exist"))
+          .thenThrow(new ForbiddenException("User is not a member of target"));
 
-      GravitinoInterceptionService gravitinoInterceptionService =
-          new GravitinoInterceptionService();
-      Class<TestOperations> testOperationsClass = TestOperations.class;
-      Method[] methods = testOperationsClass.getMethods();
-      Method testMethod = methods[0];
-      List<MethodInterceptor> methodInterceptors =
-          gravitinoInterceptionService.getMethodInterceptors(testMethod);
-      MethodInterceptor methodInterceptor = methodInterceptors.get(0);
+      Method method = TestOperations.class.getMethods()[0];

Review Comment:
   [Nit] `getMethods()[0]` does not have a defined order.
   
   `Class.getMethods()` returns public methods "in no particular order" per its 
javadoc, and the array includes the inherited `Object` methods (`equals`, 
`hashCode`, `toString`, `wait`, ...), not just `testMethod`. If the order ever 
differs, `getMethodInterceptors(method)` is handed a method with no 
`@AuthorizationExpression` and `.get(0)` throws `IndexOutOfBoundsException` 
rather than failing with a useful message.
   
   This was carried over from the old `testMetalakeNotExist`, so it is not a 
regression — but the line is being rewritten anyway, and 
`TestOperations.class.getMethod("testMethod", String.class)` is the same length 
and deterministic. The file already uses that form elsewhere (e.g. 
`tableLoadInterceptor()` at lines 1049-1054).
   
   Verified by: read the rewritten test at lines 493-528, `TestOperations` at 
lines 1029-1038, and the existing `getMethod(...)` helpers at lines 1049-1065.



##########
server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java:
##########
@@ -314,10 +304,9 @@ private Optional<Response> 
validateCurrentUserAndActiveRoles(
                       "job.namespace must identify an existing metalake: %s", 
metalakeIdent.name()),
                   e));
         }
-        // Not a real authz denial — metalake is absent, not forbidden. Skip 
event dispatch;
-        // HttpAuditFilter will emit a generic HttpRequestFailureEvent for 
this 403.
-        return Optional.of(
-            buildNoAuthResponse(expressionAnnotation, metadataContext, method, 
expression));
+        // A missing metalake is not an authorization denial, so skip the 
denial event. Return the
+        // same client-visible response as a failed membership check to avoid 
exposing existence.
+        return Optional.of(metalakeMembershipFailure(currentUser, 
metalakeIdent.name()));

Review Comment:
   [Important] The existence oracle this PR closes on path-metalake endpoints 
is still open on `POST /api/lineage`.
   
   Both catch branches now return the same neutral 403 — except when 
`dynamicMetalake` is true. Lines 300-305, just above, still return a 400 
`IllegalArguments` whose message is `job.namespace must identify an existing 
metalake: <name>`, while a caller who is simply not a member of an existing 
metalake falls through to the `ForbiddenException` branch and gets the neutral 
403. That is exactly the two-different-responses probe the PR description says 
it is removing: send `{"job": {"namespace": "<guess>"}}` and read 400 vs 403 to 
learn whether `<guess>` exists.
   
   This is reachable by any authenticated user in a default deployment:
   - `LineageOperations.postLineage` carries 
`@AuthorizationExpression(CAN_ACCESS_METADATA)` and has no 
`@AuthorizationMetadata(METALAKE)` parameter, so `metalakeIdent` is null at 
line 180 and the dynamic path at lines 224-232 is the one taken.
   - The metalake name is caller-controlled: 
`LineageAuthorizationExecutor.getAuthorizationMetalake()` returns 
`event.getJob().getNamespace()` verbatim 
(LineageAuthorizationExecutor.java:71-78).
   - `LineageConfig.SOURCE_NAME` defaults to `http` (LineageConfig.java:56-61), 
which resolves to `HTTPLineageSource`, whose REST package `GravitinoServer` 
registers at startup (GravitinoServer.java:154) — so the endpoint is on by 
default, not behind a flag.
   
   Either route the `dynamicMetalake` case through `metalakeMembershipFailure` 
as well, or, if the 400 is wanted for request-shape reasons, make it name-free 
(`job.namespace does not identify a metalake you can access`) so it carries no 
more information than the 403. Worth stating either way in the PR description, 
since "the same error code, type, and neutral message for both cases" is not 
currently true for every annotated endpoint.
   
   Verified by: read `validateCurrentUserAndActiveRoles` at 
GravitinoInterceptionService.java:288-330 and both of its call sites (lines 
180-187, 224-235); read `LineageOperations.postLineage` and confirmed it has no 
METALAKE-annotated parameter; read 
`LineageAuthorizationExecutor.getAuthorizationMetalake` (:71-78); traced 
`LineageConfig.sourceClass()` (:90-97) to `HTTPLineageSource` and its 
registration via `LineageService.getRESTPackages()` (LineageService.java:80-85) 
into `GravitinoServer.java:154`. `grep -rn "getAuthorizationMetalake"` shows 
`LineageAuthorizationExecutor` is the only non-default implementation, so 
lineage is the only endpoint affected.



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