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 49ddf140bf9a..6d3e3cc3635f 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 2ab2e56b139a..7cbe4e0c51a8 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 0ec928d87eb6..f0c5ba5563c8 100644 --- a/standalone-metastore/metastore-rest-catalog/pom.xml +++ b/standalone-metastore/metastore-rest-catalog/pom.xml @@ -41,6 +41,14 @@ hive-iceberg-catalog ${hive.version} + + + org.apache.hive + hive-exec + ${hive.version} + core + provided + org.springframework.boot @@ -70,13 +78,6 @@ 1.9.17 - - org.apache.hive - hive-exec - ${hive.version} - core - test - org.apache.hive hive-standalone-metastore-common 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 f38c2d3d406e..34e4f04d9361 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 metricsReporters; private final Clock clock = Clock.systemUTC(); - public HMSCatalogAdapter(String catalogName, Catalog catalog, List metricsReporters) { + public HMSCatalogAdapter(String catalogName, Catalog catalog, IcebergAuthorizer icebergAuthorizer, + List 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 vars, Object body) { CreateTableRequest request = castRequest(CreateTableRequest.class, body); request.validate(); if (request.stageCreate()) { + Map 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 f9ad8df0d28c..baea10f72ab5 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 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 reporters = createReporters(); - return security.proxy(new HMSCatalogServlet(new HMSCatalogAdapter(catalogName, catalog, reporters))); + return security.proxy(new HMSCatalogServlet(new HMSCatalogAdapter(catalogName, catalog, icebergAuthorizer, + reporters))); } private List 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 000000000000..2df051105b77 --- /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. + * + *

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 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 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 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 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 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 5d41d9c9b407..b7f3e0e9f73c 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 { protected abstract Optional> getPermissionTestClientConfiguration() throws Exception; + protected abstract Optional> 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 41452254db3d..9982a38fa344 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 000000000000..0d13414a0074 --- /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 ff06a53aec02..fa5236860556 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> getPermissionTestClientConfiguration() t )); } + @Override + protected Optional> 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 3414bbb0a031..e2bbc1278008 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 getDefaultClientConfiguration() { protected Optional> getPermissionTestClientConfiguration() { return Optional.empty(); } + + @Override + protected Optional> 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 df1787cdb53b..4733b526d026 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 @@ -55,6 +55,16 @@ protected Optional> getPermissionTestClientConfiguration() { )); } + @Override + protected Optional> 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 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 765ba0015bba..babb81f8b475 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 @@ -58,6 +58,16 @@ protected Optional> getPermissionTestClientConfiguration() { )); } + @Override + protected Optional> 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 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 699233c302b6..fb1e7ccd3b18 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> getPermissionTestClientConfiguration() { )); } + @Override + protected Optional> getPermissionReadOnlyClientConfiguration() { + return Optional.of(Map.of( + "uri", REST_CATALOG_EXTENSION.getRestEndpoint(), + "header.x-actor-username", MockHiveAuthorizer.PERMISSION_READ_ONLY_USER + )); + } + @Test void testWithoutUserName() { Map 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 671fcc0e4ba0..17fd47c161d9 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 4dd2600d3a6f..7bad32a5c32f 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 @@ -19,7 +19,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; @@ -35,6 +37,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; @@ -101,6 +106,33 @@ public void checkPrivileges(HiveOperationType hiveOpType, List true; + default -> false; + }; + } + + private boolean containsDeniedUri(List 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 de66d11844cc..b64553b7f3b6 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 @@ -43,6 +43,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; @@ -136,6 +138,8 @@ private void createClients(RealmResource realm, List 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 scopes) { @@ -198,6 +202,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; }