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