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

okumin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/hive.git


The following commit(s) were added to refs/heads/master by this push:
     new b88f9481fbc HIVE-29769: Authorize a stage-create on Iceberg REST API 
(#6641)
b88f9481fbc is described below

commit b88f9481fbc6ab1bc892607876d821f5a25ca396
Author: Shohei Okumiya <[email protected]>
AuthorDate: Fri Aug 14 16:43:52 2026 +0900

    HIVE-29769: Authorize a stage-create on Iceberg REST API (#6641)
    
    * Add integration tests
    
    * HIVE-29769: Authorize a stage-create on Iceberg REST API
    
    * Follow checkstyle
    
    * Split a long line
---
 .../apache/iceberg/hive/HiveTableOperations.java   |   3 +-
 .../apache/iceberg/hive/HiveViewOperations.java    |   3 +-
 .../metastore-rest-catalog/pom.xml                 |  15 +-
 .../org/apache/iceberg/rest/HMSCatalogAdapter.java |   7 +-
 .../org/apache/iceberg/rest/HMSCatalogFactory.java |   4 +-
 .../org/apache/iceberg/rest/IcebergAuthorizer.java | 164 +++++++++++++
 .../apache/iceberg/rest/BaseRESTCatalogTests.java  | 122 ++++++++++
 .../iceberg/rest/BaseRESTViewCatalogTests.java     |  39 +++
 .../apache/iceberg/rest/TestIcebergAuthorizer.java | 268 +++++++++++++++++++++
 .../iceberg/rest/TestRESTCatalogJwtAuth.java       |   8 +
 .../iceberg/rest/TestRESTCatalogNoneAuth.java      |   5 +
 .../iceberg/rest/TestRESTCatalogOAuth2Jwt.java     |  10 +
 .../TestRESTCatalogOAuth2TokenIntrospection.java   |  10 +
 .../iceberg/rest/TestRESTCatalogSimpleAuth.java    |   8 +
 .../extension/HiveRESTCatalogServerExtension.java  |   4 +
 .../iceberg/rest/extension/MockHiveAuthorizer.java |  32 +++
 .../rest/extension/OAuth2AuthorizationServer.java  |   8 +
 17 files changed, 699 insertions(+), 11 deletions(-)

diff --git 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
index 49ddf140bf9..6d3e3cc3635 100644
--- 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
+++ 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveTableOperations.java
@@ -53,6 +53,7 @@
 import org.apache.iceberg.exceptions.AlreadyExistsException;
 import org.apache.iceberg.exceptions.CommitFailedException;
 import org.apache.iceberg.exceptions.CommitStateUnknownException;
+import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.exceptions.NoSuchTableException;
 import org.apache.iceberg.exceptions.ValidationException;
 import org.apache.iceberg.hadoop.ConfigProperties;
@@ -373,7 +374,7 @@ protected void doCommit(TableMetadata base, TableMetadata 
metadata) {
       } catch (InvalidObjectException e) {
         throw new ValidationException(e, "Invalid Hive object for %s.%s", 
database, tableName);
 
-      } catch (CommitFailedException | CommitStateUnknownException e) {
+      } catch (CommitFailedException | CommitStateUnknownException | 
ForbiddenException e) {
         throw e;
 
       } catch (Throwable e) {
diff --git 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
index 2ab2e56b139..7cbe4e0c51a 100644
--- 
a/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
+++ 
b/iceberg/iceberg-catalog/src/main/java/org/apache/iceberg/hive/HiveViewOperations.java
@@ -37,6 +37,7 @@
 import org.apache.iceberg.exceptions.AlreadyExistsException;
 import org.apache.iceberg.exceptions.CommitFailedException;
 import org.apache.iceberg.exceptions.CommitStateUnknownException;
+import org.apache.iceberg.exceptions.ForbiddenException;
 import org.apache.iceberg.exceptions.NoSuchViewException;
 import org.apache.iceberg.exceptions.ValidationException;
 import org.apache.iceberg.hadoop.ConfigProperties;
@@ -205,7 +206,7 @@ public void doCommit(ViewMetadata base, ViewMetadata 
metadata) {
       } catch (InvalidObjectException e) {
         throw new ValidationException(e, "Invalid Hive object for %s.%s", 
database, viewName);
 
-      } catch (CommitFailedException | CommitStateUnknownException e) {
+      } catch (CommitFailedException | CommitStateUnknownException | 
ForbiddenException e) {
         throw e;
 
       } catch (Throwable e) {
diff --git a/standalone-metastore/metastore-rest-catalog/pom.xml 
b/standalone-metastore/metastore-rest-catalog/pom.xml
index 32367d7a438..edeb7de1d73 100644
--- a/standalone-metastore/metastore-rest-catalog/pom.xml
+++ b/standalone-metastore/metastore-rest-catalog/pom.xml
@@ -47,6 +47,14 @@
       <artifactId>hive-iceberg-catalog</artifactId>
       <version>${hive.version}</version>
     </dependency>
+    <!-- We need hive-exec to bring Hive's authorization plugin -->
+    <dependency>
+      <groupId>org.apache.hive</groupId>
+      <artifactId>hive-exec</artifactId>
+      <version>${hive.version}</version>
+      <classifier>core</classifier>
+      <scope>provided</scope>
+    </dependency>
     <!-- Spring Boot dependencies - use Jetty instead of Tomcat for Java 21 
compatibility -->
     <dependency>
       <groupId>org.springframework.boot</groupId>
@@ -76,13 +84,6 @@
       <version>1.9.17</version>
     </dependency>
     <!-- Test dependencies -->
-    <dependency>
-      <groupId>org.apache.hive</groupId>
-      <artifactId>hive-exec</artifactId>
-      <version>${hive.version}</version>
-      <classifier>core</classifier>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>org.apache.hive</groupId>
       <artifactId>hive-standalone-metastore-common</artifactId>
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
index 93e93744c23..885e3006352 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogAdapter.java
@@ -113,16 +113,19 @@ public class HMSCatalogAdapter implements Closeable {
   private final Catalog catalog;
   private final SupportsNamespaces asNamespaceCatalog;
   private final ViewCatalog asViewCatalog;
+  private final IcebergAuthorizer icebergAuthorizer;
   private final List<IcebergMetricsReporter> metricsReporters;
   private final Clock clock = Clock.systemUTC();
 
-  public HMSCatalogAdapter(String catalogName, Catalog catalog, 
List<IcebergMetricsReporter> metricsReporters) {
+  public HMSCatalogAdapter(String catalogName, Catalog catalog, 
IcebergAuthorizer icebergAuthorizer,
+      List<IcebergMetricsReporter> metricsReporters) {
     Preconditions.checkArgument(catalog instanceof SupportsNamespaces);
     Preconditions.checkArgument(catalog instanceof ViewCatalog);
     this.catalogName = catalogName;
     this.catalog = catalog;
     this.asNamespaceCatalog = (SupportsNamespaces) catalog;
     this.asViewCatalog = (ViewCatalog) catalog;
+    this.icebergAuthorizer = icebergAuthorizer;
     this.metricsReporters = metricsReporters;
   }
 
@@ -284,6 +287,8 @@ private LoadTableResponse createTable(Map<String, String> 
vars, Object body) {
     CreateTableRequest request = castRequest(CreateTableRequest.class, body);
     request.validate();
     if (request.stageCreate()) {
+      Map<String, String> namespaceMetadata = 
asNamespaceCatalog.loadNamespaceMetadata(namespace);
+      icebergAuthorizer.validateStageCreateTable(catalogName, namespace, 
namespaceMetadata, request);
       return castResponse(
               responseType, CatalogHandlers.stageTableCreate(catalog, 
namespace, request));
     } else {
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
index 9b08348f32e..6f169424626 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/HMSCatalogFactory.java
@@ -116,8 +116,10 @@ private HttpServlet createServlet(Catalog catalog) {
     List<String> scopes = Collections.singletonList("catalog");
     ServletSecurity security = new 
ServletSecurity(AuthType.fromString(authType), configuration, req -> scopes);
     String catalogName = MetastoreConf.getVar(configuration, 
ConfVars.CATALOG_DEFAULT);
+    IcebergAuthorizer icebergAuthorizer = new IcebergAuthorizer(configuration);
     List<IcebergMetricsReporter> reporters = createReporters();
-    return security.proxy(new HMSCatalogServlet(new 
HMSCatalogAdapter(catalogName, catalog, reporters)));
+    var adapter = new HMSCatalogAdapter(catalogName, catalog, 
icebergAuthorizer, reporters);
+    return security.proxy(new HMSCatalogServlet(adapter));
   }
 
   private List<IcebergMetricsReporter> createReporters() {
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/IcebergAuthorizer.java
 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/IcebergAuthorizer.java
new file mode 100644
index 00000000000..2df051105b7
--- /dev/null
+++ 
b/standalone-metastore/metastore-rest-catalog/src/main/java/org/apache/iceberg/rest/IcebergAuthorizer.java
@@ -0,0 +1,164 @@
+/*
+ * 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.iceberg.rest;
+
+import static org.apache.iceberg.hive.HiveCatalog.HMS_DB_OWNER;
+import static org.apache.iceberg.hive.HiveCatalog.HMS_DB_OWNER_TYPE;
+import static org.apache.iceberg.hive.HiveCatalog.HMS_TABLE_OWNER;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Supplier;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Preconditions;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.metastore.api.PrincipalType;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
+import org.apache.hadoop.hive.ql.metadata.HiveException;
+import org.apache.hadoop.hive.ql.metadata.HiveUtils;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAccessControlException;
+import org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthorizer;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthzContext;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthzPluginException;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthzSessionContext;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveMetastoreClientFactoryImpl;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveOperationType;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.metastore.HiveMetaStoreAuthorizer;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.hive.HiveHadoopUtil;
+import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Performs authorization checks for Iceberg REST Catalog operations that do 
not reach Hive Metastore.
+ *
+ * <p>Most catalog operations eventually call Hive Metastore and are 
authorized there. Some operations, such as
+ * stage-create, return metadata without creating a metastore object, so the 
corresponding metastore authorization
+ * hooks are not invoked. This class performs the required checks before those 
operations are processed.
+ */
+class IcebergAuthorizer {
+  private static final Logger LOG = 
LoggerFactory.getLogger(IcebergAuthorizer.class);
+
+  @VisibleForTesting
+  final Supplier<HiveAuthorizer> authorizerSupplier;
+
+  IcebergAuthorizer(Configuration conf) {
+    final var classes = MetastoreConf.getTrimmedStringsVar(conf, 
MetastoreConf.ConfVars.PRE_EVENT_LISTENERS);
+    if (classes.length == 0) {
+      LOG.info("No pre-event listeners configured, skipping authorization 
checks");
+      this.authorizerSupplier = () -> null;
+      return;
+    }
+    if 
(Arrays.stream(classes).noneMatch(HiveMetaStoreAuthorizer.class.getName()::equals))
 {
+      throw new IllegalArgumentException(
+          "HiveMetaStoreAuthorizer is required when pre-event listeners are 
configured, but %s is configured"
+              .formatted(Arrays.toString(classes)));
+    }
+
+    this.authorizerSupplier = () -> {
+      try {
+        final var hiveConf = HiveConf.cloneConf(conf);
+        final var authorizerFactory = HiveUtils.getAuthorizerFactory(hiveConf,
+            HiveConf.ConfVars.HIVE_AUTHORIZATION_MANAGER);
+
+        final var authenticator = HiveUtils.getAuthenticator(hiveConf,
+            HiveConf.ConfVars.HIVE_METASTORE_AUTHENTICATOR_MANAGER);
+        authenticator.setConf(hiveConf);
+
+        final var authzContextBuilder = new HiveAuthzSessionContext.Builder();
+        
authzContextBuilder.setClientType(HiveAuthzSessionContext.CLIENT_TYPE.HIVEMETASTORE);
+        authzContextBuilder.setSessionString("IcebergRESTCatalog");
+        return authorizerFactory.createHiveAuthorizer(
+            new HiveMetastoreClientFactoryImpl(hiveConf), hiveConf, 
authenticator, authzContextBuilder.build());
+      } catch (HiveException e) {
+        throw new IllegalStateException("Failed to initialize Hive authorizer 
for Iceberg REST Catalog", e);
+      }
+    };
+  }
+
+  @VisibleForTesting
+  IcebergAuthorizer(Supplier<HiveAuthorizer> authorizerSupplier) {
+    this.authorizerSupplier = authorizerSupplier;
+  }
+
+  /**
+   * Enforces authorization similar to [CreateTableEvent]. Checking the 
DFS_URI privilege for the location is critical;
+   * without it, Credential Vending becomes a ticket service that allows end 
users to access arbitrary locations.
+   * Checking DATABASE or TABLE_OR_VIEW privileges are nice to have. Without 
them, end users would notice missing
+   * privileges after writing data files.
+   *
+   * @param catalogName the Hive catalog name
+   * @param namespace the Iceberg namespace
+   * @param namespaceMetadata the Iceberg namespace metadata
+   * @param request the create table request
+   * @throws ForbiddenException if the user does not have the required 
privileges
+   * @throws IllegalStateException if the authorization plugin fails
+   */
+  void validateStageCreateTable(String catalogName, Namespace namespace, 
Map<String, String> namespaceMetadata,
+      CreateTableRequest request) {
+    Preconditions.checkArgument(request.stageCreate(), "Only stage create 
requests are supported");
+    Preconditions.checkArgument(namespace.levels().length == 1, "Hive does not 
support multi-level namespaces");
+    var databaseName = namespace.level(0);
+    var authorizer = authorizerSupplier.get();
+    if (authorizer == null) {
+      LOG.info("No pre-event listener is configured, skipping stage-create 
authorization");
+      return;
+    }
+
+    List<HivePrivilegeObject> inputs = request.location() == null
+        ? Collections.emptyList()
+        : Collections.singletonList(new 
HivePrivilegeObject(HivePrivilegeObject.HivePrivilegeObjectType.DFS_URI,
+            request.location()));
+    final String currentUser = HiveHadoopUtil.currentUser();
+    String databaseOwnerName = currentUser;
+    final PrincipalType databaseOwnerType;
+    if (namespaceMetadata.get(HMS_DB_OWNER) == null) {
+      databaseOwnerType = PrincipalType.USER;
+    } else {
+      databaseOwnerName = namespaceMetadata.get(HMS_DB_OWNER);
+      var rawOwnerType = namespaceMetadata.get(HMS_DB_OWNER_TYPE);
+      databaseOwnerType = rawOwnerType == null ? null : 
PrincipalType.valueOf(rawOwnerType);
+    }
+    final String tableOwnerName = 
request.properties().getOrDefault(HMS_TABLE_OWNER, currentUser);
+    List<HivePrivilegeObject> outputs = List.of(
+        new 
HivePrivilegeObject(HivePrivilegeObject.HivePrivilegeObjectType.DATABASE, 
catalogName, databaseName, null,
+            null, null, HivePrivilegeObject.HivePrivObjectActionType.OTHER, 
null, null,
+            databaseOwnerName, databaseOwnerType),
+        new 
HivePrivilegeObject(HivePrivilegeObject.HivePrivilegeObjectType.TABLE_OR_VIEW, 
catalogName, databaseName,
+            request.name(), null, null, 
HivePrivilegeObject.HivePrivObjectActionType.OTHER, null, null,
+            tableOwnerName, PrincipalType.USER)
+    );
+
+    var builder = new HiveAuthzContext.Builder();
+    builder.setCommandString("create table " + request.name());
+    try {
+      authorizer.checkPrivileges(HiveOperationType.CREATETABLE, inputs, 
outputs, builder.build());
+    } catch (HiveAccessControlException e) {
+      throw new ForbiddenException(e, e.getMessage());
+    } catch (HiveAuthzPluginException e) {
+      throw new IllegalStateException("Failed to check privileges 
stage-create", e);
+    }
+  }
+}
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTCatalogTests.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTCatalogTests.java
index d56c50b61a3..206d72c9cb1 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTCatalogTests.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTCatalogTests.java
@@ -24,14 +24,20 @@
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Optional;
+import org.apache.iceberg.BaseTable;
 import org.apache.iceberg.PartitionSpec;
 import org.apache.iceberg.Schema;
+import org.apache.iceberg.Table;
 import org.apache.iceberg.TableMetadata;
+import org.apache.iceberg.Transaction;
+import org.apache.iceberg.catalog.Catalog;
 import org.apache.iceberg.catalog.CatalogTests;
 import org.apache.iceberg.catalog.Namespace;
 import org.apache.iceberg.catalog.TableCommit;
 import org.apache.iceberg.catalog.TableIdentifier;
 import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.exceptions.NoSuchTableException;
+import org.apache.iceberg.rest.extension.MockHiveAuthorizer;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
@@ -48,6 +54,8 @@ abstract class BaseRESTCatalogTests extends 
CatalogTests<RESTCatalog> {
 
   protected abstract Optional<Map<String, String>> 
getPermissionTestClientConfiguration() throws Exception;
 
+  protected abstract Optional<Map<String, String>> 
getPermissionReadOnlyClientConfiguration() throws Exception;
+
   @BeforeAll
   void setupAll() throws Exception {
     catalog = RCKUtils.initCatalogClient(getDefaultClientConfiguration());
@@ -155,4 +163,118 @@ void testPermissionsWithDeniedUser() throws Exception {
       throw new AssertionError("Catalog operation failed", e);
     }
   }
+
+  @Test
+  void testPermissionsWithReadOnlyUser() throws Exception {
+    var properties = getPermissionReadOnlyClientConfiguration();
+    if (properties.isEmpty()) {
+      return;
+    }
+    var db = Namespace.of("permission_read_only_db");
+    var emptyDb = Namespace.of("permission_read_only_db_empty");
+    var table = TableIdentifier.of(db, "test_table");
+    var view = TableIdentifier.of(db, "test_view");
+    try (var client = 
RCKUtils.initCatalogClient(getDefaultClientConfiguration())) {
+      client.createNamespace(db);
+      client.createNamespace(emptyDb);
+      client.createTable(table, new Schema());
+      client.buildView(view).withQuery("hive", "SELECT count(*) FROM 
default.permission_test")
+          .withSchema(new Schema()).withDefaultNamespace(db).create();
+    } catch (IOException e) {
+      throw new AssertionError("Catalog operation failed", e);
+    }
+    try (var client = RCKUtils.initCatalogClient(properties.get())) {
+      // Should this fail?
+      Assertions.assertTrue(client.listNamespaces().contains(db));
+      Assertions.assertTrue(client.namespaceExists(db));
+      Assertions.assertNotNull(client.loadNamespaceMetadata(db));
+      testUnauthorizedAccess(() -> 
client.createNamespace(Namespace.of("new-db")));
+      testUnauthorizedAccess(() -> client.dropNamespace(emptyDb));
+      testUnauthorizedAccess(() -> client.setProperties(db, 
Collections.singletonMap("key", "value")));
+      testUnauthorizedAccess(() -> client.removeProperties(db, 
Collections.singleton("key")));
+
+      // Should this fail?
+      Assertions.assertEquals(Collections.singletonList(table), 
client.listTables(db));
+      Assertions.assertTrue(client.tableExists(table));
+      Assertions.assertNotNull(client.loadTable(table));
+      testUnauthorizedAccess(() -> client.createTable(TableIdentifier.of(db, 
"new-table"), new Schema()));
+      testUnauthorizedAccess(() -> client.renameTable(table, 
TableIdentifier.of(db, "new-table")));
+      testUnauthorizedAccess(() -> client.dropTable(table));
+
+      // Should this fail?
+      Assertions.assertEquals(Collections.singletonList(view), 
client.listViews(db));
+      Assertions.assertTrue(client.viewExists(view));
+      Assertions.assertNotNull(client.loadView(view));
+      testUnauthorizedAccess(() -> client.buildView(TableIdentifier.of(db, 
"new-view"))
+          .withQuery("hive", "SELECT count(*) FROM 
default.permission_test").withSchema(new Schema())
+          .withDefaultNamespace(db).create());
+      testUnauthorizedAccess(() -> client.renameView(view, 
TableIdentifier.of(db, "new-view")));
+      testUnauthorizedAccess(() -> client.dropView(view));
+
+      testUnauthorizedAccess(() -> 
client.newCreateTableTransaction(TableIdentifier.of(db, "test"),
+          new Schema()));
+      testUnauthorizedAccess(() -> 
client.newReplaceTableTransaction(TableIdentifier.of(db, "test"),
+          new Schema(), true));
+
+      var loadedTable = (BaseTable) client.loadTable(table);
+      TableMetadata base = loadedTable.operations().current();
+      TableMetadata updated = 
TableMetadata.buildFrom(base).setProperties(Map.of("key", "value")).build();
+      testUnauthorizedAccess(() -> 
client.commitTransaction(TableCommit.create(table, base, updated)));
+    } catch (IOException e) {
+      throw new AssertionError("Catalog operation failed", e);
+    }
+  }
+
+  @Test
+  void testCreateTableWithDefaultLocation() {
+    var tableIdentifier = TableIdentifier.of("default", 
"create-table-default");
+    Table table = catalog.buildTable(tableIdentifier, new Schema()).create();
+    
Assertions.assertTrue(table.location().contains("/external/create-table-default-"));
+    Assertions.assertEquals(table.location(), 
catalog.loadTable(tableIdentifier).location());
+  }
+
+  @Test
+  void testCreateTableWithAllowedLocation() {
+    var tableIdentifier = TableIdentifier.of("default", 
"create-table-allowed");
+    var location = MockHiveAuthorizer.ALLOWED_PREFIX + "/create-table-allowed";
+    Table table = catalog.buildTable(tableIdentifier, new 
Schema()).withLocation(location).create();
+    Assertions.assertEquals(location, table.location());
+    Assertions.assertEquals(table.location(), 
catalog.loadTable(tableIdentifier).location());
+  }
+
+  @Test
+  void testCreateTableWithDeniedLocation() {
+    var tableIdentifier = TableIdentifier.of("default", "create-table-denied");
+    var location = MockHiveAuthorizer.DENIED_PREFIX + "/create-table-denied";
+    Catalog.TableBuilder builder = catalog.buildTable(tableIdentifier, new 
Schema()).withLocation(location);
+    Assertions.assertThrows(ForbiddenException.class, builder::create);
+    Assertions.assertThrows(NoSuchTableException.class, () -> 
catalog.loadTable(tableIdentifier));
+  }
+
+  @Test
+  void testStageCreateTableWithDefaultLocation() {
+    var tableIdentifier = TableIdentifier.of("default", 
"stage-create-table-default");
+    Transaction transaction = catalog.buildTable(tableIdentifier, new 
Schema()).createTransaction();
+    
Assertions.assertTrue(transaction.table().location().contains("/external/stage-create-table-default-"));
+    Assertions.assertThrows(NoSuchTableException.class, () -> 
catalog.loadTable(tableIdentifier));
+  }
+
+  @Test
+  void testStageCreateTableWithAllowedLocation() {
+    var tableIdentifier = TableIdentifier.of("default", 
"stage-create-table-allowed");
+    var location = MockHiveAuthorizer.ALLOWED_PREFIX + 
"/stage-create-table-allowed";
+    Transaction transaction = catalog.buildTable(tableIdentifier, new 
Schema()).withLocation(location)
+        .createTransaction();
+    Assertions.assertEquals(location, transaction.table().location());
+    Assertions.assertThrows(NoSuchTableException.class, () -> 
catalog.loadTable(tableIdentifier));
+  }
+
+  @Test
+  void testStageCreateTableWithDeniedLocation() {
+    var tableIdentifier = TableIdentifier.of("default", 
"stage-create-table-denied");
+    var location = MockHiveAuthorizer.DENIED_PREFIX + 
"/stage-create-table-denied";
+    Catalog.TableBuilder builder = catalog.buildTable(tableIdentifier, new 
Schema()).withLocation(location);
+    Assertions.assertThrows(ForbiddenException.class, 
builder::createTransaction);
+    Assertions.assertThrows(NoSuchTableException.class, () -> 
catalog.loadTable(tableIdentifier));
+  }
 }
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTViewCatalogTests.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTViewCatalogTests.java
index 11ca1a4f439..e97ea2fe099 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTViewCatalogTests.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/BaseRESTViewCatalogTests.java
@@ -21,12 +21,20 @@
 
 import java.util.Collections;
 import java.util.Map;
+import org.apache.iceberg.Schema;
 import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.exceptions.NoSuchViewException;
+import org.apache.iceberg.rest.extension.MockHiveAuthorizer;
+import org.apache.iceberg.view.View;
+import org.apache.iceberg.view.ViewBuilder;
 import org.apache.iceberg.view.ViewCatalogTests;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 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)
@@ -70,4 +78,35 @@ protected boolean requiresNamespaceCreate() {
   protected boolean supportsServerSideRetry() {
     return true;
   }
+
+  private ViewBuilder buildView(TableIdentifier identifier) {
+    return catalog.buildView(identifier).withQuery("hive", "SELECT count(*) 
FROM default.permission_test")
+        .withSchema(new 
Schema()).withDefaultNamespace(Namespace.of("default"));
+  }
+
+  @Test
+  void testCreateViewWithDefaultLocation() {
+    var tableIdentifier = TableIdentifier.of("default", "create-view-default");
+    View view = buildView(tableIdentifier).create();
+    
Assertions.assertTrue(view.location().contains("/external/create-view-default-"));
+    Assertions.assertEquals(view.location(), 
catalog.loadView(tableIdentifier).location());
+  }
+
+  @Test
+  void testCreateViewWithAllowedLocation() {
+    var tableIdentifier = TableIdentifier.of("default", "create-view-allowed");
+    var location = MockHiveAuthorizer.ALLOWED_PREFIX + "/create-view-allowed";
+    View view = buildView(tableIdentifier).withLocation(location).create();
+    Assertions.assertEquals(location, view.location());
+    Assertions.assertEquals(view.location(), 
catalog.loadView(tableIdentifier).location());
+  }
+
+  @Test
+  void testCreateViewWithDeniedLocation() {
+    var tableIdentifier = TableIdentifier.of("default", "create-view-denied");
+    var location = MockHiveAuthorizer.DENIED_PREFIX + "/create-view-denied";
+    ViewBuilder builder = buildView(tableIdentifier).withLocation(location);
+    Assertions.assertThrows(ForbiddenException.class, builder::create);
+    Assertions.assertThrows(NoSuchViewException.class, () -> 
catalog.loadView(tableIdentifier));
+  }
 }
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestIcebergAuthorizer.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestIcebergAuthorizer.java
new file mode 100644
index 00000000000..0d13414a007
--- /dev/null
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestIcebergAuthorizer.java
@@ -0,0 +1,268 @@
+/*
+ * 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.iceberg.rest;
+
+import static 
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject.HivePrivObjectActionType;
+import static 
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject.HivePrivilegeObjectType;
+import static org.apache.iceberg.hive.HiveCatalog.HMS_DB_OWNER;
+import static org.apache.iceberg.hive.HiveCatalog.HMS_DB_OWNER_TYPE;
+import static org.apache.iceberg.hive.HiveCatalog.HMS_TABLE_OWNER;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.verify;
+
+import java.util.List;
+import java.util.Map;
+import org.apache.hadoop.conf.Configuration;
+import org.apache.hadoop.hive.metastore.api.PrincipalType;
+import org.apache.hadoop.hive.metastore.conf.MetastoreConf;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAccessControlException;
+import org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthorizer;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthzContext;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAuthzPluginException;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveOperationType;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HivePrivilegeObject;
+import 
org.apache.hadoop.hive.ql.security.authorization.plugin.metastore.HiveMetaStoreAuthorizer;
+import org.apache.hadoop.security.UserGroupInformation;
+import org.apache.iceberg.Schema;
+import org.apache.iceberg.catalog.Namespace;
+import org.apache.iceberg.exceptions.ForbiddenException;
+import org.apache.iceberg.rest.extension.MockHiveAuthorizer;
+import org.apache.iceberg.rest.extension.MockHiveAuthorizerFactory;
+import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+class TestIcebergAuthorizer {
+  private static final String CATALOG_NAME = "hive";
+  private static final Namespace NAMESPACE = Namespace.of("db");
+  private static final String TABLE_NAME = "table";
+  private static final String LOCATION = "file:/warehouse/db/table";
+
+  private static CreateTableRequest stageCreateRequest(String location, String 
tableOwnerName) {
+    var builder = CreateTableRequest.builder()
+        .withName(TABLE_NAME)
+        .withLocation(location)
+        .withSchema(new Schema())
+        .stageCreate();
+    if (tableOwnerName != null) {
+      builder.setProperty(HMS_TABLE_OWNER, tableOwnerName);
+    }
+    return builder.build();
+  }
+
+  @Test
+  void testConstructorWithPreEventListenerAndAuthorizer() {
+    var conf = new Configuration(false);
+    conf.set(MetastoreConf.ConfVars.PRE_EVENT_LISTENERS.getVarname(), 
HiveMetaStoreAuthorizer.class.getName());
+    conf.set(MetastoreConf.ConfVars.HIVE_AUTHORIZATION_MANAGER.getVarname(), 
MockHiveAuthorizerFactory.class.getName());
+    var icebergAuthorizer = new IcebergAuthorizer(conf);
+    Assertions.assertEquals(MockHiveAuthorizer.class, 
icebergAuthorizer.authorizerSupplier.get().getClass());
+  }
+
+  @Test
+  void testConstructorWithAdditionalPreEventListener() {
+    var conf = new Configuration(false);
+    conf.set(
+        MetastoreConf.ConfVars.PRE_EVENT_LISTENERS.getVarname(),
+        
"org.apache.hadoop.hive.ql.security.authorization.AuthorizationPreEventListener,"
+            + HiveMetaStoreAuthorizer.class.getName()
+    );
+    conf.set(MetastoreConf.ConfVars.HIVE_AUTHORIZATION_MANAGER.getVarname(), 
MockHiveAuthorizerFactory.class.getName());
+    var icebergAuthorizer = new IcebergAuthorizer(conf);
+    Assertions.assertEquals(MockHiveAuthorizer.class, 
icebergAuthorizer.authorizerSupplier.get().getClass());
+  }
+
+  @Test
+  void testConstructorWithoutPreEventListener() {
+    var conf = new Configuration(false);
+    conf.set(MetastoreConf.ConfVars.HIVE_AUTHORIZATION_MANAGER.getVarname(), 
MockHiveAuthorizerFactory.class.getName());
+    var icebergAuthorizer = new IcebergAuthorizer(conf);
+    Assertions.assertNull(icebergAuthorizer.authorizerSupplier.get());
+  }
+
+  @Test
+  void testConstructorWithIncompatiblePreEventListener() {
+    var conf = new Configuration(false);
+    conf.set(
+        MetastoreConf.ConfVars.PRE_EVENT_LISTENERS.getVarname(),
+        
"org.apache.hadoop.hive.ql.security.authorization.AuthorizationPreEventListener"
+    );
+    var exception = Assertions.assertThrows(IllegalArgumentException.class, () 
-> new IcebergAuthorizer(conf));
+    Assertions.assertEquals(
+        "HiveMetaStoreAuthorizer is required when pre-event listeners are 
configured, " +
+            "but 
[org.apache.hadoop.hive.ql.security.authorization.AuthorizationPreEventListener]
 is configured",
+        exception.getMessage()
+    );
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void testValidateStageCreateWithLocationAndNamespaceOwner() throws Exception 
{
+    var hiveAuthorizer = mock(HiveAuthorizer.class);
+    var icebergAuthorizer = new IcebergAuthorizer(() -> hiveAuthorizer);
+    var databaseOwner = "database_owner";
+    var namespaceMetadata = Map.of(
+        HMS_DB_OWNER, databaseOwner,
+        HMS_DB_OWNER_TYPE, PrincipalType.ROLE.name()
+    );
+
+    var tableOwner = "table_owner";
+    icebergAuthorizer.validateStageCreateTable(
+        CATALOG_NAME, NAMESPACE, namespaceMetadata, 
stageCreateRequest(LOCATION, tableOwner));
+
+    var operation = ArgumentCaptor.forClass(HiveOperationType.class);
+    var inputs = ArgumentCaptor.forClass(List.class);
+    var outputs = ArgumentCaptor.forClass(List.class);
+    var context = ArgumentCaptor.forClass(HiveAuthzContext.class);
+    verify(hiveAuthorizer).checkPrivileges(operation.capture(), 
inputs.capture(), outputs.capture(), context.capture());
+
+    Assertions.assertEquals(HiveOperationType.CREATETABLE, 
operation.getValue());
+
+    Assertions.assertEquals(1, inputs.getValue().size());
+    var location = (HivePrivilegeObject) inputs.getValue().getFirst();
+    assertThat(location.getType()).isEqualTo(HivePrivilegeObjectType.DFS_URI);
+    assertThat(location.getObjectName()).isEqualTo(LOCATION);
+    
assertThat(location.getActionType()).isEqualTo(HivePrivObjectActionType.OTHER);
+
+    Assertions.assertEquals(2, outputs.getValue().size());
+    var output1 = (HivePrivilegeObject) outputs.getValue().getFirst();
+    Assertions.assertEquals(HivePrivilegeObjectType.DATABASE, 
output1.getType());
+    Assertions.assertEquals(CATALOG_NAME, output1.getCatName());
+    Assertions.assertEquals(NAMESPACE.level(0), output1.getDbname());
+    Assertions.assertEquals(databaseOwner, output1.getOwnerName());
+    Assertions.assertEquals(PrincipalType.ROLE, output1.getOwnerType());
+    Assertions.assertEquals(HivePrivObjectActionType.OTHER, 
output1.getActionType());
+
+    var output2 = (HivePrivilegeObject) outputs.getValue().getLast();
+    Assertions.assertEquals(HivePrivilegeObjectType.TABLE_OR_VIEW, 
output2.getType());
+    Assertions.assertEquals(CATALOG_NAME, output2.getCatName());
+    Assertions.assertEquals(NAMESPACE.level(0), output2.getDbname());
+    Assertions.assertEquals(TABLE_NAME, output2.getObjectName());
+    Assertions.assertEquals(tableOwner, output2.getOwnerName());
+    Assertions.assertEquals(PrincipalType.USER, output2.getOwnerType());
+    Assertions.assertEquals(HivePrivObjectActionType.OTHER, 
output2.getActionType());
+
+    Assertions.assertEquals("create table " + TABLE_NAME, 
context.getValue().getCommandString());
+  }
+
+  @Test
+  @SuppressWarnings("unchecked")
+  void testValidateStageCreateWithoutLocationOrNamespaceOwner() throws 
Exception {
+    var hiveAuthorizer = mock(HiveAuthorizer.class);
+    var icebergAuthorizer = new IcebergAuthorizer(() -> hiveAuthorizer);
+
+    icebergAuthorizer.validateStageCreateTable(CATALOG_NAME, NAMESPACE, 
Map.of(), stageCreateRequest(null, null));
+
+    var operation = ArgumentCaptor.forClass(HiveOperationType.class);
+    var inputs = ArgumentCaptor.forClass(List.class);
+    var outputs = ArgumentCaptor.forClass(List.class);
+    var context = ArgumentCaptor.forClass(HiveAuthzContext.class);
+    verify(hiveAuthorizer).checkPrivileges(operation.capture(), 
inputs.capture(), outputs.capture(), context.capture());
+
+    Assertions.assertEquals(HiveOperationType.CREATETABLE, 
operation.getValue());
+
+    Assertions.assertEquals(List.of(), inputs.getValue());
+
+    Assertions.assertEquals(2, outputs.getValue().size());
+    var output1 = (HivePrivilegeObject) outputs.getValue().getFirst();
+    Assertions.assertEquals(HivePrivilegeObjectType.DATABASE, 
output1.getType());
+    Assertions.assertEquals(CATALOG_NAME, output1.getCatName());
+    Assertions.assertEquals(NAMESPACE.level(0), output1.getDbname());
+    var expectedUserName = 
UserGroupInformation.getCurrentUser().getShortUserName();
+    Assertions.assertEquals(expectedUserName, output1.getOwnerName());
+    Assertions.assertEquals(PrincipalType.USER, output1.getOwnerType());
+    Assertions.assertEquals(HivePrivObjectActionType.OTHER, 
output1.getActionType());
+
+    var output2 = (HivePrivilegeObject) outputs.getValue().getLast();
+    Assertions.assertEquals(HivePrivilegeObjectType.TABLE_OR_VIEW, 
output2.getType());
+    Assertions.assertEquals(CATALOG_NAME, output2.getCatName());
+    Assertions.assertEquals(NAMESPACE.level(0), output2.getDbname());
+    Assertions.assertEquals(TABLE_NAME, output2.getObjectName());
+    Assertions.assertEquals(expectedUserName, output2.getOwnerName());
+    Assertions.assertEquals(PrincipalType.USER, output2.getOwnerType());
+    Assertions.assertEquals(HivePrivObjectActionType.OTHER, 
output2.getActionType());
+
+    Assertions.assertEquals("create table " + TABLE_NAME, 
context.getValue().getCommandString());
+  }
+
+  @Test
+  void testValidateStageCreateTableWithoutAuthorizer() {
+    var icebergAuthorizer = new IcebergAuthorizer(() -> null);
+    icebergAuthorizer.validateStageCreateTable(CATALOG_NAME, NAMESPACE, 
Map.of(), stageCreateRequest(LOCATION, null));
+  }
+
+  @Test
+  void testValidateStageCreateTableWithNonStageCreateRequest() {
+    var hiveAuthorizer = mock(HiveAuthorizer.class);
+    var icebergAuthorizer = new IcebergAuthorizer(() -> hiveAuthorizer);
+    var request = 
CreateTableRequest.builder().withName(TABLE_NAME).withSchema(new 
Schema()).build();
+
+    var exception = Assertions.assertThrows(IllegalArgumentException.class, () 
->
+        icebergAuthorizer.validateStageCreateTable(CATALOG_NAME, NAMESPACE, 
Map.of(), request));
+    Assertions.assertEquals("Only stage create requests are supported", 
exception.getMessage());
+    Mockito.verifyNoInteractions(hiveAuthorizer);
+  }
+
+  @Test
+  void testValidateStageCreateTableWithMultiLevelNamespace() {
+    var hiveAuthorizer = mock(HiveAuthorizer.class);
+    var icebergAuthorizer = new IcebergAuthorizer(() -> hiveAuthorizer);
+
+    var nestedNamespace = Namespace.of("db", "nested");
+    var request = stageCreateRequest(LOCATION, null);
+    var exception = Assertions.assertThrows(IllegalArgumentException.class, () 
->
+        icebergAuthorizer.validateStageCreateTable(CATALOG_NAME, 
nestedNamespace, Map.of(), request));
+    Assertions.assertEquals("Hive does not support multi-level namespaces", 
exception.getMessage());
+    Mockito.verifyNoInteractions(hiveAuthorizer);
+  }
+
+  @Test
+  void testValidateStageCreateTableRejected() throws Exception {
+    var hiveAuthorizer = mock(HiveAuthorizer.class);
+    var failure = new HiveAccessControlException("access denied");
+    doThrow(failure).when(hiveAuthorizer).checkPrivileges(any(), anyList(), 
anyList(), any());
+    var icebergAuthorizer = new IcebergAuthorizer(() -> hiveAuthorizer);
+
+    var request = stageCreateRequest(LOCATION, null);
+    var exception = Assertions.assertThrows(ForbiddenException.class, () -> 
icebergAuthorizer
+        .validateStageCreateTable(CATALOG_NAME, NAMESPACE, Map.of(), request));
+    Assertions.assertEquals("access denied", exception.getMessage());
+    Assertions.assertSame(failure, exception.getCause());
+  }
+
+  @Test
+  void testTranslateAuthorizationPluginException() throws Exception {
+    HiveAuthorizer hiveAuthorizer = mock(HiveAuthorizer.class);
+    HiveAuthzPluginException failure = new HiveAuthzPluginException("plugin 
failure");
+    doThrow(failure).when(hiveAuthorizer).checkPrivileges(any(), anyList(), 
anyList(), any());
+    IcebergAuthorizer icebergAuthorizer = new IcebergAuthorizer(() -> 
hiveAuthorizer);
+
+    var request = stageCreateRequest(LOCATION, null);
+    var exception = Assertions.assertThrows(IllegalStateException.class, () ->
+        icebergAuthorizer.validateStageCreateTable(CATALOG_NAME, NAMESPACE, 
Map.of(), request));
+    Assertions.assertEquals("Failed to check privileges stage-create", 
exception.getMessage());
+    Assertions.assertSame(failure, exception.getCause());
+  }
+}
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogJwtAuth.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogJwtAuth.java
index 00eba38a937..273ffe0b3b0 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogJwtAuth.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogJwtAuth.java
@@ -54,6 +54,14 @@ protected Optional<Map<String, String>> 
getPermissionTestClientConfiguration() t
     ));
   }
 
+  @Override
+  protected Optional<Map<String, String>> 
getPermissionReadOnlyClientConfiguration() throws Exception {
+    return Optional.of(Map.of(
+        "uri", REST_CATALOG_EXTENSION.getRestEndpoint(),
+        "token", 
JwksServer.generateValidJWT(MockHiveAuthorizer.PERMISSION_READ_ONLY_USER)
+    ));
+  }
+
   @Test
   void testWithUnauthorizedKey() throws Exception {
     // "token" is a parameter for OAuth 2.0 Bearer token authentication. We 
use it to pass a JWT token
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogNoneAuth.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogNoneAuth.java
index 889c329a98f..049c5012bd2 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogNoneAuth.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogNoneAuth.java
@@ -44,4 +44,9 @@ protected Map<String, String> getDefaultClientConfiguration() 
{
   protected Optional<Map<String, String>> 
getPermissionTestClientConfiguration() {
     return Optional.empty();
   }
+
+  @Override
+  protected Optional<Map<String, String>> 
getPermissionReadOnlyClientConfiguration() {
+    return Optional.empty();
+  }
 }
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2Jwt.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2Jwt.java
index 5e9f026fec3..21504a6732d 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2Jwt.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2Jwt.java
@@ -56,6 +56,16 @@ protected Optional<Map<String, String>> 
getPermissionTestClientConfiguration() {
     ));
   }
 
+  @Override
+  protected Optional<Map<String, String>> 
getPermissionReadOnlyClientConfiguration() {
+    return Optional.of(Map.of(
+        "uri", REST_CATALOG_EXTENSION.getRestEndpoint(),
+        "rest.auth.type", "oauth2",
+        "oauth2-server-uri", REST_CATALOG_EXTENSION.getOAuth2TokenEndpoint(),
+        "credential", 
REST_CATALOG_EXTENSION.getOAuth2ClientCredentialForPermissionReadOnly()
+    ));
+  }
+
   @Test
   void testWithAccessToken() {
     Map<String, String> properties = Map.of(
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2TokenIntrospection.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2TokenIntrospection.java
index 8ef90fdde4c..fed9d70729c 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2TokenIntrospection.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogOAuth2TokenIntrospection.java
@@ -59,6 +59,16 @@ protected Optional<Map<String, String>> 
getPermissionTestClientConfiguration() {
     ));
   }
 
+  @Override
+  protected Optional<Map<String, String>> 
getPermissionReadOnlyClientConfiguration() {
+    return Optional.of(Map.of(
+        "uri", REST_CATALOG_EXTENSION.getRestEndpoint(),
+        "rest.auth.type", "oauth2",
+        "oauth2-server-uri", REST_CATALOG_EXTENSION.getOAuth2TokenEndpoint(),
+        "credential", 
REST_CATALOG_EXTENSION.getOAuth2ClientCredentialForPermissionReadOnly()
+    ));
+  }
+
   @Test
   void testWithAccessToken() {
     Map<String, String> properties = Map.of(
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogSimpleAuth.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogSimpleAuth.java
index 5c325b1084e..f2d62186862 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogSimpleAuth.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/TestRESTCatalogSimpleAuth.java
@@ -53,6 +53,14 @@ protected Optional<Map<String, String>> 
getPermissionTestClientConfiguration() {
     ));
   }
 
+  @Override
+  protected Optional<Map<String, String>> 
getPermissionReadOnlyClientConfiguration() {
+    return Optional.of(Map.of(
+        "uri", REST_CATALOG_EXTENSION.getRestEndpoint(),
+        "header.x-actor-username", MockHiveAuthorizer.PERMISSION_READ_ONLY_USER
+    ));
+  }
+
   @Test
   void testWithoutUserName() {
     Map<String, String> properties = Map.of(
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/HiveRESTCatalogServerExtension.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/HiveRESTCatalogServerExtension.java
index 696092790ea..03374c6072f 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/HiveRESTCatalogServerExtension.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/HiveRESTCatalogServerExtension.java
@@ -142,6 +142,10 @@ public String getOAuth2ClientCredentialForPermissionTest() 
{
     return authorizationServer.getClientCredentialForPermissionTest();
   }
 
+  public String getOAuth2ClientCredentialForPermissionReadOnly() {
+    return authorizationServer.getClientCredentialForPermissionReadOnly();
+  }
+
   public String getOAuth2AccessToken() {
     return authorizationServer.getAccessToken();
   }
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/MockHiveAuthorizer.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/MockHiveAuthorizer.java
index 3aa56ef8469..86b1cde870e 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/MockHiveAuthorizer.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/MockHiveAuthorizer.java
@@ -20,7 +20,9 @@
 package org.apache.iceberg.rest.extension;
 
 import java.util.List;
+import org.apache.hadoop.fs.Path;
 import org.apache.hadoop.hive.conf.HiveConf;
+import org.apache.hadoop.hive.metastore.MetaStoreTestUtils;
 import org.apache.hadoop.hive.ql.security.HiveAuthenticationProvider;
 import 
org.apache.hadoop.hive.ql.security.authorization.plugin.AbstractHiveAuthorizer;
 import 
org.apache.hadoop.hive.ql.security.authorization.plugin.HiveAccessControlException;
@@ -36,6 +38,9 @@
 
 public class MockHiveAuthorizer extends AbstractHiveAuthorizer {
   public static final String PERMISSION_TEST_USER = "permission_test_user";
+  public static final String PERMISSION_READ_ONLY_USER = 
"permission_read_only_user";
+  public static final String ALLOWED_PREFIX = 
MetaStoreTestUtils.getTestWarehouseDir("allowed");
+  public static final String DENIED_PREFIX = 
MetaStoreTestUtils.getTestWarehouseDir("denied");
   private static final Logger LOG = 
LoggerFactory.getLogger(MockHiveAuthorizer.class);
 
   private final HiveAuthenticationProvider authenticator;
@@ -102,6 +107,33 @@ public void checkPrivileges(HiveOperationType hiveOpType, 
List<HivePrivilegeObje
       throw new HiveAccessControlException(String.format("Unauthorized. 
Operation=%s, inputs=%s, outputs=%s",
           hiveOpType, inputsHObjs, outputHObjs));
     }
+    if (PERMISSION_READ_ONLY_USER.equals(authenticator.getUserName()) && 
isWriteOperation(hiveOpType)) {
+      throw new HiveAccessControlException(String.format(
+          "Unauthorized write operations. Operation=%s, inputs=%s, outputs=%s",
+          hiveOpType, inputsHObjs, outputHObjs));
+    }
+    if (containsDeniedUri(inputsHObjs)) {
+      throw new HiveAccessControlException(String.format("Unauthorized URI. 
Operation=%s, inputs=%s, outputs=%s",
+          hiveOpType, inputsHObjs, outputHObjs));
+    }
+  }
+
+  private boolean isWriteOperation(HiveOperationType type) {
+    return switch (type) {
+    case CREATEDATABASE, DROPDATABASE, ALTERDATABASE, CREATETABLE, DROPTABLE, 
ALTERTABLE_ADDCOLS -> true;
+    default -> false;
+    };
+  }
+
+  private boolean containsDeniedUri(List<HivePrivilegeObject> objects) {
+    return objects.stream().anyMatch(this::containsDeniedUri);
+  }
+
+  private boolean containsDeniedUri(HivePrivilegeObject priv) {
+    if (priv.getType() != HivePrivilegeObject.HivePrivilegeObjectType.DFS_URI) 
{
+      return false;
+    }
+    return priv.getObjectName().startsWith(DENIED_PREFIX + Path.SEPARATOR);
   }
 
   @Override
diff --git 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/OAuth2AuthorizationServer.java
 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/OAuth2AuthorizationServer.java
index 66ac4953580..c6c40608e25 100644
--- 
a/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/OAuth2AuthorizationServer.java
+++ 
b/standalone-metastore/metastore-rest-catalog/src/test/java/org/apache/iceberg/rest/extension/OAuth2AuthorizationServer.java
@@ -44,6 +44,8 @@ public class OAuth2AuthorizationServer {
   private static final String ICEBERG_CLIENT_SECRET = "iceberg-client-secret";
   private static final String ICEBERG_CLIENT_ID_PERMISSION_TEST = 
"iceberg-client-permission-test";
   private static final String ICEBERG_CLIENT_SECRET_PERMISSION_TEST = 
"iceberg-client-secret-permission-test";
+  private static final String ICEBERG_CLIENT_ID_PERMISSION_READ_ONLY = 
"iceberg-client-permission-read-only";
+  private static final String ICEBERG_CLIENT_SECRET_PERMISSION_READ_ONLY = 
"iceberg-client-secret-permission-read-only";
 
   private GenericContainer<?> container;
   private Keycloak keycloak;
@@ -137,6 +139,8 @@ private void createClients(RealmResource realm, 
List<String> scopes, ProtocolMap
         ICEBERG_CLIENT_SECRET);
     createClient(realm, scopes, List.of(audience, 
createEmailClaim(MockHiveAuthorizer.PERMISSION_TEST_USER)),
         ICEBERG_CLIENT_ID_PERMISSION_TEST, 
ICEBERG_CLIENT_SECRET_PERMISSION_TEST);
+    createClient(realm, scopes, List.of(audience, 
createEmailClaim(MockHiveAuthorizer.PERMISSION_READ_ONLY_USER)),
+        ICEBERG_CLIENT_ID_PERMISSION_READ_ONLY, 
ICEBERG_CLIENT_SECRET_PERMISSION_READ_ONLY);
   }
 
   private static String getAccessToken(String url, List<String> scopes) {
@@ -199,6 +203,10 @@ public String getClientCredentialForPermissionTest() {
     return "%s:%s".formatted(ICEBERG_CLIENT_ID_PERMISSION_TEST, 
ICEBERG_CLIENT_SECRET_PERMISSION_TEST);
   }
 
+  public String getClientCredentialForPermissionReadOnly() {
+    return "%s:%s".formatted(ICEBERG_CLIENT_ID_PERMISSION_READ_ONLY, 
ICEBERG_CLIENT_SECRET_PERMISSION_READ_ONLY);
+  }
+
   public String getAccessToken() {
     return accessToken;
   }

Reply via email to