This is an automated email from the ASF dual-hosted git repository.

yuqi1129 pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/branch-1.3 by this push:
     new afde98bf64 [Cherry-pick to branch-1.3] [#13096] fix(trino-connector): 
Reuse service metadata when the user token is missing (#13097) (#13102)
afde98bf64 is described below

commit afde98bf641f3e9a66eda27a06d10e8ceba2040e
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Sat Sep 12 07:27:35 2026 +0800

    [Cherry-pick to branch-1.3] [#13096] fix(trino-connector): Reuse service 
metadata when the user token is missing (#13097) (#13102)
    
    **Cherry-pick Information:**
    - Original commit: 77eae699f6b436c3d733052d9fe1c683b4274c0c
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Yuhui <[email protected]>
---
 docs/trino-connector/authentication.md             |  34 +++----
 .../trino/connector/GravitinoConnector.java        |   6 ++
 .../TestGravitinoConnectorForwardUser.java         | 102 ++++++++++++++++++++-
 3 files changed, 125 insertions(+), 17 deletions(-)

diff --git a/docs/trino-connector/authentication.md 
b/docs/trino-connector/authentication.md
index e7166b3542..d95c926c1b 100644
--- a/docs/trino-connector/authentication.md
+++ b/docs/trino-connector/authentication.md
@@ -60,8 +60,8 @@ gravitino.client.basic.password=YourSecureGravitinoPassword
 | Property                          | Description                              
                       | Default value | Required                   |
 
|-----------------------------------|-----------------------------------------------------------------|---------------|----------------------------|
 | `gravitino.client.authType`       | Authentication type: `simple`, `basic`, 
`oauth2`, or `kerberos` | (none)        | Yes (to enable Basic)      |
-| `gravitino.client.basic.username` | Local user store username                
                           | (none)        | Yes if authType is `basic` |
-| `gravitino.client.basic.password` | Local user store password                
                           | (none)        | Yes if authType is `basic` |
+| `gravitino.client.basic.username` | Local user store username                
                       | (none)        | Yes if authType is `basic` |
+| `gravitino.client.basic.password` | Local user store password                
                       | (none)        | Yes if authType is `basic` |
 
 ### OAuth2 Authentication
 
@@ -154,7 +154,7 @@ 
gravitino.client.kerberos.keytabFilePath=/path/to/user.keytab
 
 ## Session Credential Forwarding
 
-Setting `gravitino.client.session.forwardUser=true` creates a dedicated 
Gravitino client per Trino session user, so each user is visible in the 
Gravitino audit log instead of the shared `gravitino.user` or service identity. 
It is supported with `authType=simple` and `authType=oauth2`.
+Setting `gravitino.client.session.forwardUser=true` creates a dedicated 
Gravitino client per Trino session user, so each user is visible in the 
Gravitino audit log instead of the shared `gravitino.user` or service identity. 
It is supported with `authType=simple` and `authType=oauth2`. For OAuth2 
sessions without a forwarded token, the connector reuses the shared service 
metadata instead.
 
 **Configuration (`authType=simple`):**
 
@@ -184,7 +184,7 @@ gravitino.client.oauth2.scope=gravitino
 gravitino.client.session.forwardUser=true
 ```
 
-With `authType=oauth2`, the end user's IdP access token is presented to 
Gravitino directly instead of the shared client-credentials identity. This 
requires the Trino coordinator to populate the session's extra-credentials with 
the caller's access token under the key `token`; the connector reads it from 
there, and `buildForSession` fails with a clear error if it's missing.
+With `authType=oauth2`, the end user's IdP access token is presented to 
Gravitino directly when the Trino coordinator populates the session's 
extra-credentials with the caller's access token under the key `token` (or the 
configured `gravitino.client.session.userTokenCredentialKey`). If that 
credential is absent, empty, or whitespace-only, the connector reuses the 
shared service metadata. This allows password-authenticated sessions, including 
internal catalog-management JDBC sessions, to  [...]
 
 Whether the coordinator can populate this extra-credential depends on the 
Trino distribution:
 
@@ -215,11 +215,11 @@ which the connector does not re-route), the connector 
does not set `iceberg.rest
 
 **Configuration properties:**
 
-| Property                                                     | Description   
                                                                                
 | Default value   | Required   | Since version   |
-|--------------------------------------------------------------|--------------------------------------------------------------------------------------------------|-----------------|------------|-----------------|
-| `gravitino.client.session.forwardUser`                       | When `true` 
with `authType=simple` or `authType=oauth2`, forwards the Trino session 
user/token to Gravitino per-query   | `false`         | No         | 1.3.0      
     |
-| `gravitino.client.session.cache.maxSize`                     | Maximum 
number of per-user sessions to keep in the cache                                
       | `500`           | No         | 1.3.0           |
-| `gravitino.client.session.cache.expireAfterAccessSeconds`    | Seconds 
before an idle per-user session is evicted from the cache                       
       | `3600`          | No         | 1.3.0           |
+| Property                                                  | Description      
                                                                                
                                                                                
      | Default value | Required | Since version |
+|-----------------------------------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------|----------|---------------|
+| `gravitino.client.session.forwardUser`                    | When `true` with 
`authType=simple` or `authType=oauth2`, forwards the Trino session user/token 
to Gravitino per-query; OAuth2 sessions without a token use the shared service 
metadata | `false`       | No       | 1.3.0         |
+| `gravitino.client.session.cache.maxSize`                  | Maximum number 
of per-user sessions to keep in the cache                                       
                                                                                
        | `500`         | No       | 1.3.0         |
+| `gravitino.client.session.cache.expireAfterAccessSeconds` | Seconds before 
an idle per-user session is evicted from the cache                              
                                                                                
        | `3600`        | No       | 1.3.0         |
 
 ### Example: OAuth2 Per-User Token Forwarding
 
@@ -244,8 +244,8 @@ populate the session's extra-credentials with the caller's 
access token under th
 - **Open-source Trino**: there is currently no equivalent coordinator setting. 
Track
   [trinodb/trino discussion 
#24403](https://github.com/trinodb/trino/discussions/24403) and
   [issue #27917](https://github.com/trinodb/trino/issues/27917) for this 
feature request. Until
-  it lands upstream, this connector's `authType=oauth2` forwardUser path 
requires a Trino
-  distribution that provides this extra-credential itself.
+  it lands upstream, forwarding OAuth2 user tokens requires a Trino 
distribution that
+  provides this extra-credential itself. Sessions without it use the shared 
service metadata.
 
 **2. Gravitino server: enable OAuth2** (in `conf/gravitino.conf`):
 
@@ -273,8 +273,9 @@ gravitino.client.session.forwardUser=true
 ```
 
 The `gravitino.client.oauth2.*` properties configure the shared service 
identity used for catalog
-discovery; the per-user forwarded token (from step 1) is what each query 
actually authenticates
-with once `forwardUser=true`.
+discovery and for metadata access by sessions without a forwarded token. With
+`forwardUser=true`, sessions carrying a token from step 1 authenticate 
metadata requests with
+that token instead.
 
 **4. Create the metalake and catalog.** Create the metalake `my_metalake` 
first (via the
 Gravitino REST API, SDK, or CLI — see
@@ -304,9 +305,10 @@ call gravitino.system.create_catalog(
 );
 ```
 
-This call itself runs with the connector's own shared service identity, not 
any forwarded user
-token — `forwardUser` only affects `SELECT`/`SHOW`-style queries against the 
catalog afterward,
-not catalog registration itself. `create_catalog` both creates the catalog in 
Gravitino and loads
+The procedure uses the connector's shared service client to create the catalog 
in Gravitino.
+Catalog registration can also invoke the connector's metadata entry point; an 
internal
+password-authenticated JDBC session without a forwarded token uses the shared 
service metadata
+there. `create_catalog` both creates the catalog in Gravitino and loads
 it into Trino as its own top-level catalog — not as a schema nested under a 
single `gravitino`
 catalog. If the two `trino.bypass.iceberg.rest-catalog.*` properties above are 
omitted, the REST
 catalog keeps its own default security setting, independent of
diff --git 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
index 66f7b78036..57dc9913c6 100644
--- 
a/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
+++ 
b/trino-connector/trino-connector/src/main/java/org/apache/gravitino/trino/connector/GravitinoConnector.java
@@ -242,6 +242,12 @@ public class GravitinoConnector implements Connector {
                 GravitinoAuthProvider.USER_TOKEN_CREDENTIAL_KEY,
                 GravitinoAuthProvider.DEFAULT_USER_TOKEN_CREDENTIAL_KEY);
     String token = 
session.getIdentity().getExtraCredentials().get(credentialKey);
+    // Password-authenticated sessions have no OAuth token. Reuse the 
configured service
+    // identity in that case; failures with a supplied token must still 
propagate.
+    if (GravitinoAuthProvider.parseAuthType(authType) == 
GravitinoAuthProvider.AuthType.OAUTH2
+        && StringUtils.isBlank(token)) {
+      return connectorMetadata;
+    }
     String credKey = sessionCacheKey(authType, session.getUser(), token);
     try {
       return perUserSessionCache.get(
diff --git 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
index 29b91f8ca3..82e73441e2 100644
--- 
a/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
+++ 
b/trino-connector/trino-connector/src/test/java/org/apache/gravitino/trino/connector/TestGravitinoConnectorForwardUser.java
@@ -28,12 +28,18 @@ import static org.junit.jupiter.api.Assertions.assertSame;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableMap;
 import io.trino.spi.TrinoException;
+import io.trino.spi.connector.Connector;
+import io.trino.spi.connector.ConnectorMetadata;
 import io.trino.spi.connector.ConnectorSession;
+import io.trino.spi.connector.ConnectorTransactionHandle;
 import io.trino.spi.security.ConnectorIdentity;
+import java.util.List;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.function.Function;
 import org.apache.gravitino.Catalog;
@@ -44,6 +50,7 @@ import org.apache.gravitino.client.GravitinoMetalake;
 import org.apache.gravitino.rel.TableCatalog;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorContext;
 import org.apache.gravitino.trino.connector.catalog.CatalogConnectorMetadata;
+import 
org.apache.gravitino.trino.connector.catalog.CatalogConnectorMetadataAdapter;
 import org.apache.gravitino.trino.connector.metadata.GravitinoCatalog;
 import org.apache.gravitino.trino.connector.security.GravitinoAuthProvider;
 import org.junit.jupiter.api.Test;
@@ -51,6 +58,98 @@ import org.junit.jupiter.api.Test;
 /** Tests for forwardUser startup validation in {@link GravitinoConnector}. */
 class TestGravitinoConnectorForwardUser {
 
+  @Test
+  void testPasswordSessionUsesServiceMetadataForCatalogLifecycleAndQueries() {
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2"));
+    Connector internal = mock(Connector.class);
+    ConnectorMetadata internalMetadata = mock(ConnectorMetadata.class);
+    when(ctx.getInternalConnector()).thenReturn(internal);
+    when(internal.getMetadata(any(), any())).thenReturn(internalMetadata);
+    GravitinoConnector connector =
+        new GravitinoConnector(ctx) {
+          @Override
+          protected GravitinoMetadata createGravitinoMetadata(
+              CatalogConnectorMetadata metadata,
+              CatalogConnectorMetadataAdapter adapter,
+              ConnectorMetadata delegate) {
+            return new GravitinoMetadata(metadata, adapter, delegate) {};
+          }
+        };
+    ConnectorSession session = mockSession("gravitino_catalog_manager", "");
+    GravitinoTransactionHandle transaction =
+        new GravitinoTransactionHandle(mock(ConnectorTransactionHandle.class));
+    ConnectorMetadata metadata =
+        assertDoesNotThrow(() -> connector.getMetadata(session, transaction));
+    assertDoesNotThrow(() -> metadata.beginQuery(session));
+    assertDoesNotThrow(() -> metadata.cleanupQuery(session));
+    verify(internalMetadata).beginQuery(session);
+    verify(internalMetadata).cleanupQuery(session);
+    SupportsSchemas schemas = 
ctx.getMetalake().loadCatalog("catalog").asSchemas();
+    when(schemas.listSchemas()).thenReturn(new String[] {"test_schema"});
+    assertEquals(List.of("test_schema"), metadata.listSchemaNames(session));
+    verify(schemas).listSchemas();
+    verify(internalMetadata, never()).listSchemaNames(any());
+  }
+
+  @Test
+  void testMissingOAuthTokenReusesServiceMetadataWithoutBuildingUserClient() {
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2"));
+    GravitinoConnector connector =
+        newConnectorWithAuthClient(
+            ctx,
+            session -> {
+              throw new AssertionError("A missing token must not create a user 
client");
+            });
+    CatalogConnectorMetadata service =
+        connector.resolveSessionMetadata(mockSession("manager", null));
+    assertSame(service, connector.resolveSessionMetadata(mockSession("alice", 
"")));
+    assertSame(service, connector.resolveSessionMetadata(mockSession("bob", "  
")));
+  }
+
+  @Test
+  void testCustomTokenKeyControlsForwardingAndFallback() {
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "oauth2",
+                GravitinoAuthProvider.USER_TOKEN_CREDENTIAL_KEY, 
"custom-token"));
+    AtomicInteger count = new AtomicInteger();
+    GravitinoConnector connector =
+        newConnectorWithAuthClient(ctx, session -> 
mockAdminClient(ctx.getMetalake(), count));
+    ConnectorSession session = mockSession("alice", "ignored-default-token");
+    CatalogConnectorMetadata service = 
connector.resolveSessionMetadata(session);
+    assertEquals(0, count.get());
+    when(session.getIdentity().getExtraCredentials())
+        .thenReturn(ImmutableMap.of("custom-token", "user-token"));
+    assertNotSame(service, connector.resolveSessionMetadata(session));
+    assertEquals(1, count.get());
+  }
+
+  @Test
+  void testSimpleAuthStillForwardsUsernameWithoutToken() {
+    CatalogConnectorContext ctx =
+        mockContextWithConfig(
+            ImmutableMap.of(
+                GravitinoAuthProvider.FORWARD_SESSION_USER_KEY, "true",
+                GravitinoAuthProvider.AUTH_TYPE_KEY, "simple"));
+    AtomicInteger count = new AtomicInteger();
+    GravitinoConnector connector =
+        newConnectorWithAuthClient(ctx, session -> 
mockAdminClient(ctx.getMetalake(), count));
+    assertNotSame(
+        connector.resolveSessionMetadata(mockSession("alice", null)),
+        connector.resolveSessionMetadata(mockSession("bob", null)));
+    assertEquals(2, count.get());
+  }
+
   @Test
   void testForwardUserWithoutAuthTypeThrowsAtConstruction() {
     CatalogConnectorContext ctx =
@@ -249,7 +348,8 @@ class TestGravitinoConnectorForwardUser {
 
   private static ConnectorSession mockSession(String user, String token) {
     ConnectorIdentity identity = mock(ConnectorIdentity.class);
-    when(identity.getExtraCredentials()).thenReturn(ImmutableMap.of("token", 
token));
+    when(identity.getExtraCredentials())
+        .thenReturn(token == null ? ImmutableMap.of() : 
ImmutableMap.of("token", token));
     ConnectorSession session = mock(ConnectorSession.class);
     when(session.getUser()).thenReturn(user);
     when(session.getIdentity()).thenReturn(identity);

Reply via email to