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

roryqi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 4b9ad2ceae [#10198] feat(spark): Report required table privileges 
(#11627)
4b9ad2ceae is described below

commit 4b9ad2ceae704ecc765e21605da5be569e3acaaa
Author: roryqi <[email protected]>
AuthorDate: Tue Jun 30 19:34:03 2026 +0800

    [#10198] feat(spark): Report required table privileges (#11627)
    
    ### What changes were proposed in this pull request?
    
    - Pass explicit required privileges when Spark loads Gravitino tables.
    - Defer table authorization failures until Spark analysis completes.
    - Aggregate and report all denied tables and required privileges.
    - Register the authorization check through Spark session extensions.
    
    ### Why are the changes needed?
    
    Spark currently fails on the first table without sufficient privileges
    during relation resolution. This prevents users from seeing all
    privileges required by a query.
    
    Fix: #10198
    
    ### Does this PR introduce _any_ user-facing change?
    
    Yes. Spark queries now report all inaccessible tables and their required
    Gravitino privileges in one authorization error.
    
    ### How was this patch tested?
    
    - `./gradlew :spark-connector:spark-common:test`
    - Compiled production and test sources for Spark 3.3, 3.4, and 3.5.
    - Added tests for privilege propagation, proxy delegation, aggregated
    reporting, and extension registration.
---
 .../authorization/AuthorizationTable.java          | 193 +++++++++++++++++++++
 ...avitinoAuthorizationSparkSessionExtensions.java |  96 ++++++++++
 .../authorization/RequiredPrivilegesCheck.java     |  46 +++++
 .../spark/connector/catalog/BaseCatalog.java       |  35 +++-
 .../connector/plugin/GravitinoDriverPlugin.java    |   8 +-
 .../authorization/TestAuthorizationTable.java      | 101 +++++++++++
 .../authorization/TestRequiredPrivilegesCheck.java |  75 ++++++++
 .../TestRequiredPrivilegesSparkResolution.java     | 184 ++++++++++++++++++++
 .../catalog/TestBaseCatalogAuthorization.java      | 177 +++++++++++++++++++
 .../plugin/TestGravitinoDriverPlugin.java          |  27 +++
 10 files changed, 938 insertions(+), 4 deletions(-)

diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/AuthorizationTable.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/AuthorizationTable.java
new file mode 100644
index 0000000000..73f94706e6
--- /dev/null
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/AuthorizationTable.java
@@ -0,0 +1,193 @@
+/*
+ * 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.spark.connector.authorization;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.stream.Collectors;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.spark.sql.connector.catalog.SupportsRead;
+import org.apache.spark.sql.connector.catalog.SupportsWrite;
+import org.apache.spark.sql.connector.catalog.Table;
+import org.apache.spark.sql.connector.catalog.TableCapability;
+import org.apache.spark.sql.connector.expressions.Transform;
+import org.apache.spark.sql.connector.read.ScanBuilder;
+import org.apache.spark.sql.connector.write.LogicalWriteInfo;
+import org.apache.spark.sql.connector.write.WriteBuilder;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.util.CaseInsensitiveStringMap;
+
+/**
+ * A placeholder Spark table returned when the caller lacks the Gravitino 
privileges to load a
+ * table.
+ *
+ * <p>The connector does not load the real Spark table for a denied table, 
because that load would
+ * bypass the very authorization the caller is missing. Instead {@link #deny} 
records the table and
+ * its required privileges in a per-thread collector and returns this 
placeholder so Spark's {@code
+ * ResolveRelations} can finish resolving every relation in the query. {@link
+ * RequiredPrivilegesCheck} then drains the collector once resolution 
completes and reports all
+ * denied tables together, before analysis fails on anything else.
+ *
+ * <p>The metadata methods ({@link #name}, {@link #schema}, ...) return 
harmless placeholders so the
+ * relation can be built during resolution, while the data-access methods 
({@link #newScanBuilder},
+ * {@link #newWriteBuilder}) fail closed: should the authorization check ever 
be bypassed, the table
+ * still cannot be read or written.
+ */
+public class AuthorizationTable implements Table, SupportsRead, SupportsWrite {
+
+  // Denied tables discovered while resolving the relations of a single query, 
keyed by the fully
+  // qualified table identifier. Held per-thread because Spark analyzes one 
query per thread.
+  private static final ThreadLocal<DeniedTables> DENIED_TABLES =
+      ThreadLocal.withInitial(DeniedTables::new);
+
+  private static final StructType EMPTY_SCHEMA = new StructType();
+  private static final Transform[] EMPTY_PARTITIONING = new Transform[0];
+  private static final Set<TableCapability> CAPABILITIES =
+      ImmutableSet.of(
+          TableCapability.BATCH_READ,
+          TableCapability.BATCH_WRITE,
+          TableCapability.TRUNCATE,
+          TableCapability.OVERWRITE_BY_FILTER,
+          TableCapability.OVERWRITE_DYNAMIC);
+
+  private final String name;
+  private final ForbiddenException forbiddenException;
+
+  private AuthorizationTable(String name, ForbiddenException 
forbiddenException) {
+    this.name = name;
+    this.forbiddenException = forbiddenException;
+  }
+
+  /**
+   * Records a denied table for the current thread and returns a placeholder 
table to keep in the
+   * analyzed plan.
+   *
+   * @param name the simple table name surfaced to Spark
+   * @param tableIdentifier the fully qualified Gravitino table identifier
+   * @param requiredPrivileges the privileges required to use the table
+   * @param forbiddenException the original authorization failure
+   * @return a placeholder table carrying the authorization failure
+   */
+  public static Table deny(
+      String name,
+      String tableIdentifier,
+      Set<Privilege.Name> requiredPrivileges,
+      ForbiddenException forbiddenException) {
+    DENIED_TABLES.get().record(tableIdentifier, requiredPrivileges, 
forbiddenException);
+    return new AuthorizationTable(name, forbiddenException);
+  }
+
+  /**
+   * Returns an aggregated authorization failure for every denied table 
collected on the current
+   * thread, then clears the collector. Returns {@link Optional#empty()} when 
no table was denied.
+   *
+   * @return the aggregated failure, or empty if there is none
+   */
+  public static Optional<ForbiddenException> drainFailure() {
+    try {
+      return DENIED_TABLES.get().failure();
+    } finally {
+      clear();
+    }
+  }
+
+  /** Clears the denied tables collected on the current thread. */
+  public static void clear() {
+    DENIED_TABLES.remove();
+  }
+
+  @Override
+  public String name() {
+    return name;
+  }
+
+  @Override
+  public StructType schema() {
+    return EMPTY_SCHEMA;
+  }
+
+  @Override
+  public Transform[] partitioning() {
+    return EMPTY_PARTITIONING;
+  }
+
+  @Override
+  public Map<String, String> properties() {
+    return Collections.emptyMap();
+  }
+
+  @Override
+  public Set<TableCapability> capabilities() {
+    return CAPABILITIES;
+  }
+
+  @Override
+  public ScanBuilder newScanBuilder(CaseInsensitiveStringMap options) {
+    throw forbiddenException;
+  }
+
+  @Override
+  public WriteBuilder newWriteBuilder(LogicalWriteInfo info) {
+    throw forbiddenException;
+  }
+
+  private static class DeniedTables {
+    private final Map<String, Set<Privilege.Name>> tables = new TreeMap<>();
+    private ForbiddenException firstFailure;
+
+    private void record(
+        String tableIdentifier,
+        Set<Privilege.Name> requiredPrivileges,
+        ForbiddenException forbiddenException) {
+      tables
+          .computeIfAbsent(tableIdentifier, ignored -> new TreeSet<>())
+          .addAll(requiredPrivileges);
+      if (firstFailure == null) {
+        firstFailure = forbiddenException;
+      }
+    }
+
+    private Optional<ForbiddenException> failure() {
+      if (tables.isEmpty()) {
+        return Optional.empty();
+      }
+
+      String requirements =
+          tables.entrySet().stream()
+              .map(
+                  entry ->
+                      entry.getKey()
+                          + ": "
+                          + entry.getValue().stream()
+                              .map(Privilege.Name::name)
+                              .collect(Collectors.joining(", ")))
+              .collect(Collectors.joining("; "));
+      return Optional.of(
+          new ForbiddenException(
+              firstFailure, "Missing required privileges for Spark tables: 
[%s]", requirements));
+    }
+  }
+}
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/GravitinoAuthorizationSparkSessionExtensions.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/GravitinoAuthorizationSparkSessionExtensions.java
new file mode 100644
index 0000000000..fbdaeb9ce1
--- /dev/null
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/GravitinoAuthorizationSparkSessionExtensions.java
@@ -0,0 +1,96 @@
+/*
+ * 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.spark.connector.authorization;
+
+import org.apache.spark.sql.SparkSessionExtensions;
+import org.apache.spark.sql.catalyst.FunctionIdentifier;
+import org.apache.spark.sql.catalyst.TableIdentifier;
+import org.apache.spark.sql.catalyst.expressions.Expression;
+import org.apache.spark.sql.catalyst.parser.ParseException;
+import org.apache.spark.sql.catalyst.parser.ParserInterface;
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan;
+import org.apache.spark.sql.types.DataType;
+import org.apache.spark.sql.types.StructType;
+import scala.Function1;
+import scala.collection.immutable.Seq;
+
+/** Registers Gravitino authorization checks with a Spark session. */
+public class GravitinoAuthorizationSparkSessionExtensions
+    implements Function1<SparkSessionExtensions, Void> {
+
+  @Override
+  public Void apply(SparkSessionExtensions extensions) {
+    // Post-hoc resolution runs after every relation is resolved but before 
checkAnalysis, so all
+    // denied tables are reported together rather than failing on the first 
resolution error.
+    extensions.injectPostHocResolutionRule(session -> new 
RequiredPrivilegesCheck());
+    extensions.injectParser((session, parser) -> new 
AuthorizationParser(parser));
+    return null;
+  }
+
+  private static class AuthorizationParser implements ParserInterface {
+    private final ParserInterface delegate;
+
+    private AuthorizationParser(ParserInterface delegate) {
+      this.delegate = delegate;
+    }
+
+    @Override
+    public LogicalPlan parsePlan(String sqlText) throws ParseException {
+      AuthorizationTable.clear();
+      return delegate.parsePlan(sqlText);
+    }
+
+    @Override
+    public Expression parseExpression(String sqlText) throws ParseException {
+      return delegate.parseExpression(sqlText);
+    }
+
+    @Override
+    public TableIdentifier parseTableIdentifier(String sqlText) throws 
ParseException {
+      return delegate.parseTableIdentifier(sqlText);
+    }
+
+    @Override
+    public FunctionIdentifier parseFunctionIdentifier(String sqlText) throws 
ParseException {
+      return delegate.parseFunctionIdentifier(sqlText);
+    }
+
+    @Override
+    public Seq<String> parseMultipartIdentifier(String sqlText) throws 
ParseException {
+      return delegate.parseMultipartIdentifier(sqlText).toList();
+    }
+
+    @Override
+    public LogicalPlan parseQuery(String sqlText) throws ParseException {
+      AuthorizationTable.clear();
+      return delegate.parseQuery(sqlText);
+    }
+
+    @Override
+    public StructType parseTableSchema(String sqlText) throws ParseException {
+      return delegate.parseTableSchema(sqlText);
+    }
+
+    @Override
+    public DataType parseDataType(String sqlText) throws ParseException {
+      return delegate.parseDataType(sqlText);
+    }
+  }
+}
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/RequiredPrivilegesCheck.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/RequiredPrivilegesCheck.java
new file mode 100644
index 0000000000..902e30045e
--- /dev/null
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/authorization/RequiredPrivilegesCheck.java
@@ -0,0 +1,46 @@
+/*
+ * 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.spark.connector.authorization;
+
+import java.util.Optional;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan;
+import org.apache.spark.sql.catalyst.rules.Rule;
+
+/**
+ * A post-hoc resolution rule that reports the tables a query lacks Gravitino 
privileges for.
+ *
+ * <p>It runs after Spark's {@code ResolveRelations} has resolved every 
relation in the query, so
+ * all denied tables have been collected by {@link AuthorizationTable#deny}, 
but before {@code
+ * checkAnalysis} would fail the query for an unrelated reason (such as a 
column that cannot be
+ * resolved against a denied table's placeholder schema). This lets a single 
error list every
+ * inaccessible table and its required privileges instead of failing on the 
first one.
+ */
+public class RequiredPrivilegesCheck extends Rule<LogicalPlan> {
+
+  @Override
+  public LogicalPlan apply(LogicalPlan plan) {
+    Optional<ForbiddenException> failure = AuthorizationTable.drainFailure();
+    if (failure.isPresent()) {
+      throw failure.get();
+    }
+    return plan;
+  }
+}
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
index 228ef8a395..7ebdf160a3 100644
--- 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/catalog/BaseCatalog.java
@@ -48,6 +48,7 @@ import 
org.apache.gravitino.spark.connector.SparkTableChangeConverter;
 import org.apache.gravitino.spark.connector.SparkTransformConverter;
 import 
org.apache.gravitino.spark.connector.SparkTransformConverter.DistributionAndSortOrdersInfo;
 import org.apache.gravitino.spark.connector.SparkTypeConverter;
+import org.apache.gravitino.spark.connector.authorization.AuthorizationTable;
 import org.apache.spark.sql.catalyst.analysis.NamespaceAlreadyExistsException;
 import org.apache.spark.sql.catalyst.analysis.NoSuchFunctionException;
 import org.apache.spark.sql.catalyst.analysis.NoSuchNamespaceException;
@@ -260,6 +261,15 @@ public abstract class BaseCatalog implements TableCatalog, 
SupportsNamespaces, F
     } catch (NoSuchTableException e) {
       // Not a table in Gravitino; try as a view.
       return loadViewAsTable(ident);
+    } catch (ForbiddenException e) {
+      // Do not load the underlying Spark table here: that load bypasses 
Gravitino authorization,
+      // which the caller is precisely missing. Return a standalone table that 
carries the failure
+      // so RequiredPrivilegesCheck can aggregate and report it during 
analysis.
+      return AuthorizationTable.deny(
+          ident.name(),
+          String.format("%s.%s.%s", catalogName, getDatabase(ident), 
ident.name()),
+          Sets.newHashSet(Privilege.Name.SELECT_TABLE),
+          e);
     }
     Table sparkTable = loadSparkTable(ident);
     return createSparkTable(
@@ -491,7 +501,9 @@ public abstract class BaseCatalog implements TableCatalog, 
SupportsNamespaces, F
       String database = getDatabase(ident);
       return gravitinoCatalogClient
           .asTableCatalog()
-          .loadTable(NameIdentifier.of(database, ident.name()));
+          .loadTable(
+              NameIdentifier.of(database, ident.name()),
+              Sets.newHashSet(Privilege.Name.SELECT_TABLE));
     } catch (org.apache.gravitino.exceptions.NoSuchTableException e) {
       throw new NoSuchTableException(ident);
     }
@@ -580,7 +592,26 @@ public abstract class BaseCatalog implements TableCatalog, 
SupportsNamespaces, F
 
   protected Table loadTableForWriting(Identifier ident)
       throws NoSuchTableException, ForbiddenException {
-    org.apache.gravitino.rel.Table gravitinoTable = 
loadGravitinoTableForWriting(ident);
+    org.apache.gravitino.rel.Table gravitinoTable;
+    try {
+      gravitinoTable = loadGravitinoTableForWriting(ident);
+    } catch (ForbiddenException e) {
+      // Do not load the underlying Spark table here: that load bypasses 
Gravitino authorization,
+      // which the caller is precisely missing.
+      //
+      // Unlike the read path, a write command targets a single table, so 
there is nothing to
+      // aggregate across relations. Returning an empty-schema placeholder 
here would let Spark's
+      // ResolveOutputRelation reject the write with a column-arity error 
before
+      // RequiredPrivilegesCheck runs, surfacing the wrong failure and leaving 
the recorded denial
+      // on the thread to leak into later queries. Record the denial and 
surface the aggregated
+      // authorization failure immediately, which also clears the per-thread 
collector.
+      AuthorizationTable.deny(
+          ident.name(),
+          String.format("%s.%s.%s", catalogName, getDatabase(ident), 
ident.name()),
+          Sets.newHashSet(Privilege.Name.MODIFY_TABLE),
+          e);
+      throw AuthorizationTable.drainFailure().orElse(e);
+    }
     org.apache.spark.sql.connector.catalog.Table sparkTable = 
loadSparkTable(ident);
     // Will create a catalog specific table
     return createSparkTable(
diff --git 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
index ea0e03aa9d..90e7830bff 100644
--- 
a/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
+++ 
b/spark-connector/spark-common/src/main/java/org/apache/gravitino/spark/connector/plugin/GravitinoDriverPlugin.java
@@ -46,6 +46,7 @@ import 
org.apache.gravitino.client.GravitinoClient.ClientBuilder;
 import org.apache.gravitino.client.GravitinoClientConfiguration;
 import org.apache.gravitino.client.KerberosTokenProvider;
 import org.apache.gravitino.spark.connector.GravitinoSparkConfig;
+import 
org.apache.gravitino.spark.connector.authorization.GravitinoAuthorizationSparkSessionExtensions;
 import org.apache.gravitino.spark.connector.catalog.GravitinoCatalogManager;
 import 
org.apache.gravitino.spark.connector.iceberg.extensions.GravitinoIcebergSparkSessionExtensions;
 import org.apache.gravitino.spark.connector.version.CatalogNameAdaptor;
@@ -80,7 +81,9 @@ public class GravitinoDriverPlugin implements DriverPlugin {
           GravitinoIcebergSparkSessionExtensions.class.getName(), 
ICEBERG_SPARK_EXTENSIONS);
   private final List<String> gravitinoPaimonExtensions = 
Arrays.asList(PAIMON_SPARK_EXTENSIONS);
 
-  private final List<String> gravitinoDriverExtensions = new ArrayList<>();
+  private final List<String> gravitinoDriverExtensions =
+      new ArrayList<>(
+          
Collections.singletonList(GravitinoAuthorizationSparkSessionExtensions.class.getName()));
   private boolean enableIcebergSupport = false;
   private boolean enablePaimonSupport = false;
 
@@ -173,7 +176,8 @@ public class GravitinoDriverPlugin implements DriverPlugin {
     LOG.info("Register {} catalog to Spark catalog manager.", catalogName);
   }
 
-  private void registerSqlExtensions(SparkConf conf) {
+  @VisibleForTesting
+  void registerSqlExtensions(SparkConf conf) {
     String extensionString = String.join(COMMA, gravitinoDriverExtensions);
     if (conf.contains(StaticSQLConf.SPARK_SESSION_EXTENSIONS().key())) {
       String sparkSessionExtensions = 
conf.get(StaticSQLConf.SPARK_SESSION_EXTENSIONS().key());
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestAuthorizationTable.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestAuthorizationTable.java
new file mode 100644
index 0000000000..38d9a645fe
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestAuthorizationTable.java
@@ -0,0 +1,101 @@
+/*
+ * 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.spark.connector.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Optional;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.spark.sql.connector.catalog.SupportsRead;
+import org.apache.spark.sql.connector.catalog.SupportsWrite;
+import org.apache.spark.sql.connector.catalog.Table;
+import org.apache.spark.sql.connector.catalog.TableCapability;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+public class TestAuthorizationTable {
+
+  @AfterEach
+  void clearCollector() {
+    AuthorizationTable.clear();
+  }
+
+  @Test
+  void testDenyCollectsAndAggregatesDeniedTables() {
+    ForbiddenException firstCause = new ForbiddenException("denied table_a");
+    Table table =
+        AuthorizationTable.deny(
+            "table_a",
+            "catalog.schema.table_a",
+            ImmutableSet.of(Privilege.Name.SELECT_TABLE),
+            firstCause);
+    AuthorizationTable.deny(
+        "table_b",
+        "catalog.schema.table_b",
+        ImmutableSet.of(Privilege.Name.MODIFY_TABLE),
+        new ForbiddenException("denied table_b"));
+
+    assertTrue(table instanceof SupportsRead);
+    assertTrue(table instanceof SupportsWrite);
+    assertTrue(table.capabilities().contains(TableCapability.BATCH_READ));
+    assertTrue(table.capabilities().contains(TableCapability.BATCH_WRITE));
+
+    Optional<ForbiddenException> failure = AuthorizationTable.drainFailure();
+    assertTrue(failure.isPresent());
+    assertEquals(
+        "Missing required privileges for Spark tables: "
+            + "[catalog.schema.table_a: SELECT_TABLE; catalog.schema.table_b: 
MODIFY_TABLE]",
+        failure.get().getMessage());
+    assertSame(firstCause, failure.get().getCause());
+
+    // The collector is drained, so a subsequent query starts clean.
+    assertFalse(AuthorizationTable.drainFailure().isPresent());
+  }
+
+  @Test
+  void testFailsClosedWhenAccessed() {
+    ForbiddenException cause = new ForbiddenException("denied");
+    Table table =
+        AuthorizationTable.deny(
+            "table_a",
+            "catalog.schema.table_a",
+            ImmutableSet.of(Privilege.Name.SELECT_TABLE),
+            cause);
+
+    assertSame(
+        cause,
+        assertThrows(ForbiddenException.class, () -> ((SupportsRead) 
table).newScanBuilder(null)));
+    assertSame(
+        cause,
+        assertThrows(
+            ForbiddenException.class, () -> ((SupportsWrite) 
table).newWriteBuilder(null)));
+  }
+
+  @Test
+  void testDrainFailureEmptyWhenNothingDenied() {
+    assertFalse(AuthorizationTable.drainFailure().isPresent());
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestRequiredPrivilegesCheck.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestRequiredPrivilegesCheck.java
new file mode 100644
index 0000000000..73c082206c
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestRequiredPrivilegesCheck.java
@@ -0,0 +1,75 @@
+/*
+ * 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.spark.connector.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.spark.sql.catalyst.plans.logical.LogicalPlan;
+import org.apache.spark.sql.catalyst.plans.logical.OneRowRelation;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+
+public class TestRequiredPrivilegesCheck {
+
+  @AfterEach
+  void clearCollector() {
+    AuthorizationTable.clear();
+  }
+
+  @Test
+  void testReportsAllDeniedTablesInDeterministicOrder() {
+    AuthorizationTable.deny(
+        "table_b",
+        "catalog.schema.table_b",
+        ImmutableSet.of(Privilege.Name.MODIFY_TABLE),
+        new ForbiddenException("denied table_b"));
+    AuthorizationTable.deny(
+        "table_a",
+        "catalog.schema.table_a",
+        ImmutableSet.of(Privilege.Name.SELECT_TABLE),
+        new ForbiddenException("denied table_a"));
+    AuthorizationTable.deny(
+        "table_a",
+        "catalog.schema.table_a",
+        ImmutableSet.of(Privilege.Name.MODIFY_TABLE),
+        new ForbiddenException("denied table_a"));
+
+    LogicalPlan plan = new OneRowRelation();
+    ForbiddenException exception =
+        assertThrows(ForbiddenException.class, () -> new 
RequiredPrivilegesCheck().apply(plan));
+
+    assertEquals(
+        "Missing required privileges for Spark tables: "
+            + "[catalog.schema.table_a: MODIFY_TABLE, SELECT_TABLE; "
+            + "catalog.schema.table_b: MODIFY_TABLE]",
+        exception.getMessage());
+  }
+
+  @Test
+  void testReturnsPlanUnchangedWhenNothingDenied() {
+    LogicalPlan plan = new OneRowRelation();
+    assertSame(plan, new RequiredPrivilegesCheck().apply(plan));
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestRequiredPrivilegesSparkResolution.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestRequiredPrivilegesSparkResolution.java
new file mode 100644
index 0000000000..c6a41bc393
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/authorization/TestRequiredPrivilegesSparkResolution.java
@@ -0,0 +1,184 @@
+/*
+ * 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.spark.connector.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Map;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.spark.sql.SparkSession;
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.Table;
+import org.apache.spark.sql.connector.catalog.TableCatalog;
+import org.apache.spark.sql.connector.catalog.TableChange;
+import org.apache.spark.sql.connector.expressions.Transform;
+import org.apache.spark.sql.internal.StaticSQLConf;
+import org.apache.spark.sql.types.StructType;
+import org.apache.spark.sql.util.CaseInsensitiveStringMap;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+/**
+ * Drives a real Spark analyzer to prove how {@link AuthorizationTable} and 
{@link
+ * RequiredPrivilegesCheck} behave end to end: every relation is resolved 
first (Spark calls {@code
+ * schema()} on each denied table while building the relation), and the 
post-hoc resolution rule
+ * then reports all denied tables together, before analysis fails for any 
other reason.
+ */
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestRequiredPrivilegesSparkResolution {
+
+  private SparkSession spark;
+
+  @BeforeAll
+  void startSpark() {
+    spark =
+        SparkSession.builder()
+            .master("local[1]")
+            .appName("test-authorization-resolution")
+            .config("spark.ui.enabled", "false")
+            .config("spark.sql.catalog.denied", 
DeniedTableCatalog.class.getName())
+            .config(
+                StaticSQLConf.SPARK_SESSION_EXTENSIONS().key(),
+                GravitinoAuthorizationSparkSessionExtensions.class.getName())
+            .getOrCreate();
+  }
+
+  @AfterAll
+  void stopSpark() {
+    if (spark != null) {
+      spark.stop();
+    }
+    AuthorizationTable.clear();
+  }
+
+  @AfterEach
+  void clearCollector() {
+    AuthorizationTable.clear();
+  }
+
+  @Test
+  void testSelectAcrossDeniedTablesReportsEveryTable() {
+    ForbiddenException failure =
+        assertThrows(
+            ForbiddenException.class,
+            () -> spark.sql("SELECT 1 FROM denied.db.t1 UNION ALL SELECT 1 
FROM denied.db.t2"));
+
+    // Both relations were resolved before the failure was raised, so a single 
error lists them all.
+    assertTrue(failure.getMessage().contains("denied.db.t1: SELECT_TABLE"), 
failure.getMessage());
+    assertTrue(failure.getMessage().contains("denied.db.t2: SELECT_TABLE"), 
failure.getMessage());
+  }
+
+  @Test
+  void testInsertIntoDeniedTableReportsForbidden() {
+    ForbiddenException failure =
+        assertThrows(
+            ForbiddenException.class,
+            () -> spark.sql("INSERT INTO denied.db.t1 SELECT * FROM 
denied.db.t2"));
+
+    // Both the write target and the read source are denied and reported 
together.
+    assertTrue(failure.getMessage().contains("denied.db.t1: SELECT_TABLE"), 
failure.getMessage());
+    assertTrue(failure.getMessage().contains("denied.db.t2: SELECT_TABLE"), 
failure.getMessage());
+  }
+
+  @Test
+  void testColumnReferenceReportsForbiddenInsteadOfUnresolvedColumn() {
+    // The denied table exposes an empty placeholder schema, so "missing_col" 
cannot be resolved.
+    // The post-hoc rule runs before checkAnalysis, so the authorization 
failure wins over the
+    // "cannot resolve column" error.
+    ForbiddenException failure =
+        assertThrows(
+            ForbiddenException.class, () -> spark.sql("SELECT missing_col FROM 
denied.db.t1"));
+
+    assertTrue(failure.getMessage().contains("denied.db.t1: SELECT_TABLE"), 
failure.getMessage());
+  }
+
+  @Test
+  void testSqlParserClearsStaleDeniedTablesBeforeNextQuery() throws Exception {
+    AuthorizationTable.deny(
+        "stale_table",
+        "catalog.schema.stale_table",
+        ImmutableSet.of(Privilege.Name.SELECT_TABLE),
+        new ForbiddenException("denied stale table"));
+
+    spark.sessionState().sqlParser().parsePlan("SELECT 1");
+
+    assertFalse(AuthorizationTable.drainFailure().isPresent());
+  }
+
+  /** A catalog that denies every table, used to exercise the authorization 
resolution path. */
+  public static class DeniedTableCatalog implements TableCatalog {
+
+    private String name;
+
+    @Override
+    public void initialize(String name, CaseInsensitiveStringMap options) {
+      this.name = name;
+    }
+
+    @Override
+    public String name() {
+      return name;
+    }
+
+    @Override
+    public Table loadTable(Identifier ident) {
+      String tableIdentifier =
+          name + "." + String.join(".", ident.namespace()) + "." + 
ident.name();
+      return AuthorizationTable.deny(
+          ident.name(),
+          tableIdentifier,
+          ImmutableSet.of(Privilege.Name.SELECT_TABLE),
+          new ForbiddenException("denied %s", tableIdentifier));
+    }
+
+    @Override
+    public Identifier[] listTables(String[] namespace) {
+      return new Identifier[0];
+    }
+
+    @Override
+    public Table createTable(
+        Identifier ident, StructType schema, Transform[] partitions, 
Map<String, String> props) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Table alterTable(Identifier ident, TableChange... changes) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public boolean dropTable(Identifier ident) {
+      return false;
+    }
+
+    @Override
+    public void renameTable(Identifier oldIdent, Identifier newIdent) {
+      throw new UnsupportedOperationException();
+    }
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogAuthorization.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogAuthorization.java
new file mode 100644
index 0000000000..37ef2c501a
--- /dev/null
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/catalog/TestBaseCatalogAuthorization.java
@@ -0,0 +1,177 @@
+/*
+ * 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.spark.connector.catalog;
+
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Map;
+import java.util.Optional;
+import org.apache.gravitino.Catalog;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.client.GravitinoClient;
+import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.rel.TableCatalog;
+import org.apache.gravitino.spark.connector.PropertiesConverter;
+import org.apache.gravitino.spark.connector.SparkTransformConverter;
+import org.apache.gravitino.spark.connector.SparkTypeConverter;
+import org.apache.gravitino.spark.connector.authorization.AuthorizationTable;
+import org.apache.spark.sql.connector.catalog.Identifier;
+import org.apache.spark.sql.connector.catalog.Table;
+import org.apache.spark.sql.util.CaseInsensitiveStringMap;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+public class TestBaseCatalogAuthorization {
+
+  private Catalog gravitinoCatalog;
+  private TableCatalog gravitinoTableCatalog;
+  private org.apache.spark.sql.connector.catalog.TableCatalog sparkCatalog;
+  private TestCatalog catalog;
+
+  @BeforeAll
+  void initCatalogManager() {
+    GravitinoCatalogManager.create(() -> mock(GravitinoClient.class));
+  }
+
+  @AfterAll
+  void cleanupCatalogManager() {
+    GravitinoCatalogManager.get().close();
+  }
+
+  @BeforeEach
+  void setUp() {
+    gravitinoCatalog = mock(Catalog.class);
+    gravitinoTableCatalog = mock(TableCatalog.class);
+    sparkCatalog = 
mock(org.apache.spark.sql.connector.catalog.TableCatalog.class);
+    when(gravitinoCatalog.asTableCatalog()).thenReturn(gravitinoTableCatalog);
+    catalog = new TestCatalog(gravitinoCatalog, sparkCatalog);
+  }
+
+  @AfterEach
+  void clearDeniedTables() {
+    AuthorizationTable.clear();
+  }
+
+  @Test
+  void testLoadTablePassesSelectTablePrivilege() throws Exception {
+    Identifier identifier = Identifier.of(new String[] {"schema"}, "table_a");
+    NameIdentifier gravitinoIdentifier = NameIdentifier.of("schema", 
"table_a");
+    org.apache.gravitino.rel.Table gravitinoTable = 
mock(org.apache.gravitino.rel.Table.class);
+    Table sparkTable = mock(Table.class);
+    when(gravitinoTableCatalog.loadTable(
+            eq(gravitinoIdentifier), 
eq(ImmutableSet.of(Privilege.Name.SELECT_TABLE))))
+        .thenReturn(gravitinoTable);
+    when(sparkCatalog.loadTable(identifier)).thenReturn(sparkTable);
+
+    assertSame(sparkTable, catalog.loadTable(identifier));
+
+    verify(gravitinoTableCatalog)
+        .loadTable(gravitinoIdentifier, 
ImmutableSet.of(Privilege.Name.SELECT_TABLE));
+  }
+
+  @Test
+  void testDeniedReadReturnsMarkedSparkTable() throws Exception {
+    Identifier identifier = Identifier.of(new String[] {"schema"}, "table_a");
+    NameIdentifier gravitinoIdentifier = NameIdentifier.of("schema", 
"table_a");
+    when(gravitinoTableCatalog.loadTable(
+            eq(gravitinoIdentifier), 
eq(ImmutableSet.of(Privilege.Name.SELECT_TABLE))))
+        .thenThrow(new ForbiddenException("denied"));
+
+    Table result = catalog.loadTable(identifier);
+
+    // The underlying Spark table is never loaded for a denied table; a 
placeholder is returned and
+    // the required privileges are collected for RequiredPrivilegesCheck to 
report during analysis.
+    assertTrue(result instanceof AuthorizationTable);
+    Optional<ForbiddenException> failure = AuthorizationTable.drainFailure();
+    assertTrue(failure.isPresent());
+    assertTrue(failure.get().getMessage().contains("table_a: SELECT_TABLE"));
+  }
+
+  @Test
+  void testLoadTableForWritingPassesModifyTablePrivilege() throws Exception {
+    Identifier identifier = Identifier.of(new String[] {"schema"}, "table_a");
+    NameIdentifier gravitinoIdentifier = NameIdentifier.of("schema", 
"table_a");
+    org.apache.gravitino.rel.Table gravitinoTable = 
mock(org.apache.gravitino.rel.Table.class);
+    Table sparkTable = mock(Table.class);
+    when(gravitinoTableCatalog.loadTable(
+            eq(gravitinoIdentifier), 
eq(ImmutableSet.of(Privilege.Name.MODIFY_TABLE))))
+        .thenReturn(gravitinoTable);
+    when(sparkCatalog.loadTable(identifier)).thenReturn(sparkTable);
+
+    assertSame(sparkTable, catalog.loadForWriting(identifier));
+
+    verify(gravitinoTableCatalog)
+        .loadTable(gravitinoIdentifier, 
ImmutableSet.of(Privilege.Name.MODIFY_TABLE));
+  }
+
+  private static class TestCatalog extends BaseCatalog {
+
+    private TestCatalog(
+        Catalog gravitinoCatalog,
+        org.apache.spark.sql.connector.catalog.TableCatalog sparkCatalog) {
+      this.gravitinoCatalogClient = gravitinoCatalog;
+      this.sparkCatalog = sparkCatalog;
+    }
+
+    private Table loadForWriting(Identifier identifier) throws Exception {
+      return loadTableForWriting(identifier);
+    }
+
+    @Override
+    protected org.apache.spark.sql.connector.catalog.TableCatalog 
createAndInitSparkCatalog(
+        String name, CaseInsensitiveStringMap options, Map<String, String> 
properties) {
+      return sparkCatalog;
+    }
+
+    @Override
+    protected Table createSparkTable(
+        Identifier identifier,
+        org.apache.gravitino.rel.Table gravitinoTable,
+        Table sparkTable,
+        org.apache.spark.sql.connector.catalog.TableCatalog sparkCatalog,
+        PropertiesConverter propertiesConverter,
+        SparkTransformConverter sparkTransformConverter,
+        SparkTypeConverter sparkTypeConverter) {
+      return sparkTable;
+    }
+
+    @Override
+    protected PropertiesConverter getPropertiesConverter() {
+      return mock(PropertiesConverter.class);
+    }
+
+    @Override
+    protected SparkTransformConverter getSparkTransformConverter() {
+      return mock(SparkTransformConverter.class);
+    }
+  }
+}
diff --git 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
index 9111682620..7da747c98d 100644
--- 
a/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
+++ 
b/spark-connector/spark-common/src/test/java/org/apache/gravitino/spark/connector/plugin/TestGravitinoDriverPlugin.java
@@ -19,7 +19,12 @@
 
 package org.apache.gravitino.spark.connector.plugin;
 
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import 
org.apache.gravitino.spark.connector.authorization.GravitinoAuthorizationSparkSessionExtensions;
 import org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions;
+import org.apache.spark.SparkConf;
+import org.apache.spark.sql.internal.StaticSQLConf;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
@@ -31,4 +36,26 @@ public class TestGravitinoDriverPlugin {
         IcebergSparkSessionExtensions.class.getName(),
         GravitinoDriverPlugin.ICEBERG_SPARK_EXTENSIONS);
   }
+
+  @Test
+  void testAlwaysRegistersAuthorizationExtension() {
+    SparkConf sparkConf = new SparkConf(false);
+
+    new GravitinoDriverPlugin().registerSqlExtensions(sparkConf);
+
+    assertEquals(
+        GravitinoAuthorizationSparkSessionExtensions.class.getName(),
+        sparkConf.get(StaticSQLConf.SPARK_SESSION_EXTENSIONS().key()));
+  }
+
+  @Test
+  void testDoesNotDuplicateAuthorizationExtension() {
+    SparkConf sparkConf = new SparkConf(false);
+    String extension = 
GravitinoAuthorizationSparkSessionExtensions.class.getName();
+    sparkConf.set(StaticSQLConf.SPARK_SESSION_EXTENSIONS().key(), extension);
+
+    new GravitinoDriverPlugin().registerSqlExtensions(sparkConf);
+
+    assertEquals(extension, 
sparkConf.get(StaticSQLConf.SPARK_SESSION_EXTENSIONS().key()));
+  }
 }


Reply via email to