diff --git a/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/TestServices.java b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/TestServices.java new file mode 100644 index 0000000000..c9dacaf453 --- /dev/null +++ b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/TestServices.java @@ -0,0 +1,148 @@ +/* + * 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.polaris.service.dropwizard; + +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import jakarta.ws.rs.core.SecurityContext; +import java.security.Principal; +import java.time.Clock; +import java.time.Instant; +import java.util.Date; +import java.util.Map; +import java.util.Set; +import org.apache.polaris.core.PolarisCallContext; +import org.apache.polaris.core.PolarisDiagnostics; +import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; +import org.apache.polaris.core.auth.PolarisAuthorizer; +import org.apache.polaris.core.context.CallContext; +import org.apache.polaris.core.context.RealmContext; +import org.apache.polaris.core.entity.PolarisEntity; +import org.apache.polaris.core.entity.PrincipalEntity; +import org.apache.polaris.core.persistence.PolarisMetaStoreManager; +import org.apache.polaris.core.persistence.PolarisMetaStoreSession; +import org.apache.polaris.core.persistence.cache.EntityCache; +import org.apache.polaris.service.admin.PolarisServiceImpl; +import org.apache.polaris.service.admin.api.PolarisCatalogsApi; +import org.apache.polaris.service.catalog.IcebergCatalogAdapter; +import org.apache.polaris.service.catalog.api.IcebergRestCatalogApi; +import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService; +import org.apache.polaris.service.catalog.io.FileIOFactory; +import org.apache.polaris.service.config.DefaultConfigurationStore; +import org.apache.polaris.service.config.RealmEntityManagerFactory; +import org.apache.polaris.service.context.CallContextCatalogFactory; +import org.apache.polaris.service.context.PolarisCallContextCatalogFactory; +import org.apache.polaris.service.dropwizard.catalog.io.TestFileIOFactory; +import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; +import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; +import org.apache.polaris.service.task.TaskExecutor; +import org.mockito.Mockito; + +public record TestServices( + IcebergRestCatalogApi restApi, + PolarisCatalogsApi catalogsApi, + RealmContext realmContext, + SecurityContext securityContext) { + private static final RealmContext testRealm = () -> "test-realm"; + + public static TestServices inMemory(Map config) { + return inMemory(new TestFileIOFactory(), config); + } + + public static TestServices inMemory(FileIOFactory ioFactory) { + return inMemory(ioFactory, Map.of()); + } + + public static TestServices inMemory(FileIOFactory ioFactory, Map config) { + InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = + new InMemoryPolarisMetaStoreManagerFactory(); + metaStoreManagerFactory.setStorageIntegrationProvider( + new PolarisStorageIntegrationProviderImpl( + Mockito::mock, () -> GoogleCredentials.create(new AccessToken("abc", new Date())))); + + PolarisMetaStoreManager metaStoreManager = + metaStoreManagerFactory.getOrCreateMetaStoreManager(testRealm); + + EntityCache cache = new EntityCache(metaStoreManager); + RealmEntityManagerFactory realmEntityManagerFactory = + new RealmEntityManagerFactory(metaStoreManagerFactory, () -> cache) {}; + CallContextCatalogFactory callContextFactory = + new PolarisCallContextCatalogFactory( + realmEntityManagerFactory, + metaStoreManagerFactory, + Mockito.mock(TaskExecutor.class), + ioFactory); + PolarisAuthorizer authorizer = Mockito.mock(PolarisAuthorizer.class); + IcebergRestCatalogApiService service = + new IcebergCatalogAdapter( + callContextFactory, realmEntityManagerFactory, metaStoreManagerFactory, authorizer); + IcebergRestCatalogApi restApi = new IcebergRestCatalogApi(service); + + PolarisMetaStoreSession session = + metaStoreManagerFactory.getOrCreateSessionSupplier(testRealm).get(); + PolarisCallContext context = + new PolarisCallContext( + session, + Mockito.mock(PolarisDiagnostics.class), + new DefaultConfigurationStore(config), + Clock.systemDefaultZone()); + PolarisMetaStoreManager.CreatePrincipalResult createdPrincipal = + metaStoreManager.createPrincipal( + context, + new PrincipalEntity.Builder() + .setName("test-principal") + .setCreateTimestamp(Instant.now().toEpochMilli()) + .setCredentialRotationRequiredState() + .build()); + + AuthenticatedPolarisPrincipal principal = + new AuthenticatedPolarisPrincipal( + PolarisEntity.of(createdPrincipal.getPrincipal()), Set.of()); + + SecurityContext securityContext = + new SecurityContext() { + @Override + public Principal getUserPrincipal() { + return principal; + } + + @Override + public boolean isUserInRole(String s) { + return false; + } + + @Override + public boolean isSecure() { + return true; + } + + @Override + public String getAuthenticationScheme() { + return ""; + } + }; + + PolarisCatalogsApi catalogsApi = + new PolarisCatalogsApi( + new PolarisServiceImpl(realmEntityManagerFactory, metaStoreManagerFactory, authorizer)); + + CallContext.setCurrentContext(CallContext.of(testRealm, context)); + return new TestServices(restApi, catalogsApi, testRealm, securityContext); + } +} diff --git a/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingCatalogTest.java b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingCatalogTest.java index aa724a2432..a37929a2ba 100644 --- a/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingCatalogTest.java +++ b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingCatalogTest.java @@ -18,77 +18,36 @@ */ package org.apache.polaris.service.dropwizard.admin; -import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; -import io.dropwizard.testing.ConfigOverride; -import io.dropwizard.testing.ResourceHelpers; -import io.dropwizard.testing.junit5.DropwizardAppExtension; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; import jakarta.ws.rs.core.Response; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.UUID; +import org.apache.iceberg.exceptions.ValidationException; import org.apache.polaris.core.admin.model.AwsStorageConfigInfo; import org.apache.polaris.core.admin.model.Catalog; import org.apache.polaris.core.admin.model.CatalogProperties; import org.apache.polaris.core.admin.model.CreateCatalogRequest; import org.apache.polaris.core.admin.model.StorageConfigInfo; -import org.apache.polaris.service.dropwizard.PolarisApplication; -import org.apache.polaris.service.dropwizard.config.PolarisApplicationConfig; -import org.apache.polaris.service.dropwizard.test.PolarisConnectionExtension; -import org.apache.polaris.service.dropwizard.test.PolarisRealm; -import org.apache.polaris.service.dropwizard.test.TestEnvironmentExtension; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.extension.ExtendWith; +import org.apache.polaris.service.dropwizard.TestServices; +import org.apache.polaris.service.dropwizard.catalog.io.TestFileIOFactory; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -@ExtendWith({ - DropwizardExtensionsSupport.class, - TestEnvironmentExtension.class, - PolarisConnectionExtension.class -}) public class PolarisOverlappingCatalogTest { - private static final DropwizardAppExtension EXT = - new DropwizardAppExtension<>( - PolarisApplication.class, - ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), - // Bind to random port to support parallelism - ConfigOverride.config("server.applicationConnectors[0].port", "0"), - ConfigOverride.config("server.adminConnectors[0].port", "0"), - // Block overlapping catalog paths: - ConfigOverride.config("featureConfiguration.ALLOW_OVERLAPPING_CATALOG_URLS", "false")); - private static String userToken; - private static String realm; - - @BeforeAll - public static void setup( - PolarisConnectionExtension.PolarisToken adminToken, @PolarisRealm String polarisRealm) - throws IOException { - userToken = adminToken.token(); - realm = polarisRealm; - - // Set up the database location - PolarisConnectionExtension.createTestDir(realm); - } + + static TestServices services = + TestServices.inMemory( + new TestFileIOFactory(), Map.of("ALLOW_OVERLAPPING_CATALOG_URLS", "false")); private Response createCatalog(String prefix, String defaultBaseLocation, boolean isExternal) { return createCatalog(prefix, defaultBaseLocation, isExternal, new ArrayList()); } - private static Invocation.Builder request() { - return EXT.client() - .target(String.format("http://localhost:%d/api/management/v1/catalogs", EXT.getLocalPort())) - .request("application/json") - .header("Authorization", "Bearer " + userToken) - .header(REALM_PROPERTY_KEY, realm); - } - private Response createCatalog( String prefix, String defaultBaseLocation, @@ -118,9 +77,10 @@ private Response createCatalog( System.currentTimeMillis(), 1, config); - try (Response response = request().post(Entity.json(new CreateCatalogRequest(catalog)))) { - return response; - } + return services + .catalogsApi() + .createCatalog( + new CreateCatalogRequest(catalog), services.realmContext(), services.securityContext()); } @ParameterizedTest @@ -144,12 +104,14 @@ public void testBasicOverlappingCatalogs(boolean initiallyExternal, boolean late .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); // inside `root` - assertThat(createCatalog(prefix, "root/child", laterExternal)) - .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThatThrownBy(() -> createCatalog(prefix, "root/child", laterExternal)) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("One or more of its locations overlaps with an existing catalog"); // `root` is inside this - assertThat(createCatalog(prefix, "", laterExternal)) - .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThatThrownBy(() -> createCatalog(prefix, "", laterExternal)) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("One or more of its locations overlaps with an existing catalog"); } @ParameterizedTest @@ -166,17 +128,24 @@ public void testAllowedLocationOverlappingCatalogs( .returns(Response.Status.CREATED.getStatusCode(), Response::getStatus); // This DBL overlaps with initial AL - assertThat(createCatalog(prefix, "dogs", initiallyExternal, Arrays.asList("huskies", "labs"))) - .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThatThrownBy( + () -> + createCatalog(prefix, "dogs", initiallyExternal, Arrays.asList("huskies", "labs"))) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("One or more of its locations overlaps with an existing catalog"); // This AL overlaps with initial DBL - assertThat( - createCatalog( - prefix, "kingdoms", initiallyExternal, Arrays.asList("plants", "animals"))) - .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThatThrownBy( + () -> + createCatalog( + prefix, "kingdoms", initiallyExternal, Arrays.asList("plants", "animals"))) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("One or more of its locations overlaps with an existing catalog"); // This AL overlaps with an initial AL - assertThat(createCatalog(prefix, "plays", initiallyExternal, Arrays.asList("rent", "cats"))) - .returns(Response.Status.BAD_REQUEST.getStatusCode(), Response::getStatus); + assertThatThrownBy( + () -> createCatalog(prefix, "plays", initiallyExternal, Arrays.asList("rent", "cats"))) + .isInstanceOf(ValidationException.class) + .hasMessageContaining("One or more of its locations overlaps with an existing catalog"); } } diff --git a/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingTableTest.java b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingTableTest.java index 2d98a170ef..876f9b3cdc 100644 --- a/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingTableTest.java +++ b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/admin/PolarisOverlappingTableTest.java @@ -18,285 +18,177 @@ */ package org.apache.polaris.service.dropwizard.admin; -import static org.apache.polaris.service.context.DefaultRealmContextResolver.REALM_PROPERTY_KEY; +import static org.apache.polaris.core.PolarisConfiguration.ALLOW_TABLE_LOCATION_OVERLAP; +import static org.apache.polaris.core.PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION; import static org.apache.polaris.service.dropwizard.admin.PolarisAuthzTestBase.SCHEMA; import static org.assertj.core.api.Assertions.assertThat; -import io.dropwizard.testing.ConfigOverride; -import io.dropwizard.testing.ResourceHelpers; -import io.dropwizard.testing.junit5.DropwizardAppExtension; -import io.dropwizard.testing.junit5.DropwizardExtensionsSupport; -import jakarta.ws.rs.client.Entity; -import jakarta.ws.rs.client.Invocation; import jakarta.ws.rs.core.Response; -import java.util.List; +import java.util.Map; import java.util.UUID; import java.util.stream.Stream; import org.apache.iceberg.catalog.Namespace; +import org.apache.iceberg.exceptions.ForbiddenException; import org.apache.iceberg.rest.requests.CreateNamespaceRequest; import org.apache.iceberg.rest.requests.CreateTableRequest; -import org.apache.polaris.core.PolarisConfiguration; -import org.apache.polaris.core.PolarisConfigurationStore; import org.apache.polaris.core.admin.model.Catalog; import org.apache.polaris.core.admin.model.CatalogProperties; import org.apache.polaris.core.admin.model.CreateCatalogRequest; import org.apache.polaris.core.admin.model.FileStorageConfigInfo; import org.apache.polaris.core.admin.model.StorageConfigInfo; -import org.apache.polaris.service.dropwizard.PolarisApplication; -import org.apache.polaris.service.dropwizard.config.PolarisApplicationConfig; -import org.apache.polaris.service.dropwizard.test.PolarisConnectionExtension; -import org.apache.polaris.service.dropwizard.test.PolarisRealm; -import org.apache.polaris.service.dropwizard.test.TestEnvironmentExtension; -import org.junit.jupiter.api.BeforeEach; +import org.apache.polaris.service.dropwizard.TestServices; import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; -@ExtendWith({ - DropwizardExtensionsSupport.class, - TestEnvironmentExtension.class, - PolarisConnectionExtension.class -}) public class PolarisOverlappingTableTest { - private static final DropwizardAppExtension BASE_EXT = - new DropwizardAppExtension<>( - PolarisApplication.class, - ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), - // Bind to random port to support parallelism - ConfigOverride.config("server.applicationConnectors[0].port", "0"), - ConfigOverride.config("server.adminConnectors[0].port", "0"), - // Enforce table location constraints - ConfigOverride.config("featureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION", "false"), - ConfigOverride.config("featureConfiguration.ALLOW_TABLE_LOCATION_OVERLAP", "false")); - private static final DropwizardAppExtension LAX_EXT = - new DropwizardAppExtension<>( - PolarisApplication.class, - ResourceHelpers.resourceFilePath("polaris-server-integrationtest.yml"), - // Bind to random port to support parallelism - ConfigOverride.config("server.applicationConnectors[0].port", "0"), - ConfigOverride.config("server.adminConnectors[0].port", "0"), - // Relax table location constraints - ConfigOverride.config("featureConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION", "true"), - ConfigOverride.config("featureConfiguration.ALLOW_TABLE_LOCATION_OVERLAP", "true")); - - private static PolarisConnectionExtension.PolarisToken adminToken; - private static String userToken; - private static String realm; - private static String namespace; + private static final String namespace = "ns"; + private static final String catalog = "test-catalog"; private static final String baseLocation = "file:///tmp/PolarisOverlappingTableTest"; - private static final CatalogWrapper defaultCatalog = new CatalogWrapper("default"); - private static final CatalogWrapper laxCatalog = new CatalogWrapper("lax"); - private static final CatalogWrapper strictCatalog = new CatalogWrapper("strict"); - - /** Used to define a parameterized test config */ - protected record TestConfig( - DropwizardAppExtension extension, - CatalogWrapper catalogWrapper, - Response.Status response) { - public String catalog() { - return catalogWrapper.catalog; - } - - private String extensionName() { - return (extension - .getConfiguration() - .findService(PolarisConfigurationStore.class) - .getConfiguration(null, PolarisConfiguration.ALLOW_TABLE_LOCATION_OVERLAP)) - ? "lax" - : "strict"; - } - - /** Extract the first component of the catalog name; e.g. `default` from `default_123_xyz` */ - private String catalogShortName() { - int firstComponentEnd = catalog().indexOf('_'); - if (firstComponentEnd != -1) { - return catalog().substring(0, firstComponentEnd); - } else { - return catalog(); - } - } - - @Override - public String toString() { - return String.format( - "extension=%s, catalog=%s, status=%s", - extensionName(), catalogShortName(), response.toString()); - } - } - - /* Used to wrap a catalog name, so the TestConfig's final `catalog` field can be updated */ - protected static class CatalogWrapper { - public String catalog; - - public CatalogWrapper(String catalog) { - this.catalog = catalog; - } - - @Override - public String toString() { - return catalog; - } - } - - @BeforeEach - public void setup( - PolarisConnectionExtension.PolarisToken adminToken, @PolarisRealm String polarisRealm) { - userToken = adminToken.token(); - realm = polarisRealm; - defaultCatalog.catalog = String.format("default_catalog_%s", UUID.randomUUID().toString()); - laxCatalog.catalog = String.format("lax_catalog_%s", UUID.randomUUID().toString()); - strictCatalog.catalog = String.format("strict_catalog_%s", UUID.randomUUID().toString()); - for (var EXT : List.of(BASE_EXT, LAX_EXT)) { - for (var c : List.of(defaultCatalog, laxCatalog, strictCatalog)) { - CatalogProperties.Builder propertiesBuilder = - CatalogProperties.builder() - .setDefaultBaseLocation(String.format("%s/%s", baseLocation, c)); - if (!c.equals(defaultCatalog)) { - propertiesBuilder - .addProperty( - PolarisConfiguration.ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), - String.valueOf(c.equals(laxCatalog))) - .addProperty( - PolarisConfiguration.ALLOW_TABLE_LOCATION_OVERLAP.catalogConfig(), - String.valueOf(c.equals(laxCatalog))); - } - StorageConfigInfo config = - FileStorageConfigInfo.builder() - .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) - .build(); - Catalog catalogObject = - new Catalog( - Catalog.TypeEnum.INTERNAL, - c.catalog, - propertiesBuilder.build(), - 1725487592064L, - 1725487592064L, - 1, - config); - try (Response response = - request(EXT, "management/v1/catalogs") - .post(Entity.json(new CreateCatalogRequest(catalogObject)))) { - if (response.getStatus() != Response.Status.CREATED.getStatusCode()) { - throw new IllegalStateException( - "Failed to create catalog: " + response.readEntity(String.class)); - } - } - - namespace = "ns"; - CreateNamespaceRequest createNamespaceRequest = - CreateNamespaceRequest.builder().withNamespace(Namespace.of(namespace)).build(); - try (Response response = - request(EXT, String.format("catalog/v1/%s/namespaces", c)) - .post(Entity.json(createNamespaceRequest))) { - if (response.getStatus() != Response.Status.OK.getStatusCode()) { - throw new IllegalStateException( - "Failed to create namespace: " + response.readEntity(String.class)); - } - } - } - } - } - - private Response createTable( - DropwizardAppExtension extension, String catalog, String location) { + private int createTable(TestServices services, String location) { CreateTableRequest createTableRequest = CreateTableRequest.builder() - .withName("table_" + UUID.randomUUID().toString()) + .withName("table_" + UUID.randomUUID()) .withLocation(location) .withSchema(SCHEMA) .build(); - String prefix = String.format("catalog/v1/%s/namespaces/%s/tables", catalog, namespace); - try (Response response = request(extension, prefix).post(Entity.json(createTableRequest))) { - return response; + try (Response response = + services + .restApi() + .createTable( + catalog, + namespace, + createTableRequest, + null, + services.realmContext(), + services.securityContext())) { + return response.getStatus(); + } catch (ForbiddenException e) { + return Response.Status.FORBIDDEN.getStatusCode(); } } - private static Invocation.Builder request( - DropwizardAppExtension extension, String prefix) { - return extension - .client() - .target(String.format("http://localhost:%d/api/%s", extension.getLocalPort(), prefix)) - .request("application/json") - .header("Authorization", "Bearer " + userToken) - .header(REALM_PROPERTY_KEY, realm); - } - - private static Stream getTestConfigs() { + static Stream testTableLocationRestrictions() { + Map laxServices = + Map.of("ALLOW_UNSTRUCTURED_TABLE_LOCATION", "true", "ALLOW_TABLE_LOCATION_OVERLAP", "true"); + Map strictServices = + Map.of( + "ALLOW_UNSTRUCTURED_TABLE_LOCATION", "false", "ALLOW_TABLE_LOCATION_OVERLAP", "false"); + Map laxCatalog = + Map.of( + ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), + "true", + ALLOW_TABLE_LOCATION_OVERLAP.catalogConfig(), + "true"); + Map strictCatalog = + Map.of( + ALLOW_UNSTRUCTURED_TABLE_LOCATION.catalogConfig(), + "false", + ALLOW_TABLE_LOCATION_OVERLAP.catalogConfig(), + "false"); return Stream.of( - new TestConfig(BASE_EXT, defaultCatalog, Response.Status.FORBIDDEN), - new TestConfig(BASE_EXT, strictCatalog, Response.Status.FORBIDDEN), - new TestConfig(BASE_EXT, laxCatalog, Response.Status.OK), - new TestConfig(LAX_EXT, defaultCatalog, Response.Status.OK), - new TestConfig(LAX_EXT, strictCatalog, Response.Status.FORBIDDEN), - new TestConfig(LAX_EXT, laxCatalog, Response.Status.OK)); + Arguments.of(strictServices, Map.of(), Response.Status.FORBIDDEN.getStatusCode()), + Arguments.of(strictServices, strictCatalog, Response.Status.FORBIDDEN.getStatusCode()), + Arguments.of(strictServices, laxCatalog, Response.Status.OK.getStatusCode()), + Arguments.of(laxServices, Map.of(), Response.Status.OK.getStatusCode()), + Arguments.of(laxServices, strictCatalog, Response.Status.FORBIDDEN.getStatusCode()), + Arguments.of(laxServices, laxCatalog, Response.Status.OK.getStatusCode())); } @ParameterizedTest - @MethodSource("getTestConfigs") + @MethodSource() @DisplayName("Test restrictions on table locations") - void testTableLocationRestrictions(TestConfig config) { + void testTableLocationRestrictions( + Map serverConfig, + Map catalogConfig, + int expectedStatusForOverlaps) { + TestServices services = TestServices.inMemory(serverConfig); + + CatalogProperties.Builder propertiesBuilder = + CatalogProperties.builder() + .setDefaultBaseLocation(String.format("%s/%s", baseLocation, catalog)) + .putAll(catalogConfig); + + StorageConfigInfo config = + FileStorageConfigInfo.builder() + .setStorageType(StorageConfigInfo.StorageTypeEnum.FILE) + .build(); + Catalog catalogObject = + new Catalog( + Catalog.TypeEnum.INTERNAL, + catalog, + propertiesBuilder.build(), + 1725487592064L, + 1725487592064L, + 1, + config); + try (Response response = + services + .catalogsApi() + .createCatalog( + new CreateCatalogRequest(catalogObject), + services.realmContext(), + services.securityContext())) { + assertThat(response.getStatus()).isEqualTo(Response.Status.CREATED.getStatusCode()); + } + + CreateNamespaceRequest createNamespaceRequest = + CreateNamespaceRequest.builder().withNamespace(Namespace.of(namespace)).build(); + try (Response response = + services + .restApi() + .createNamespace( + catalog, + createNamespaceRequest, + services.realmContext(), + services.securityContext())) { + assertThat(response.getStatus()).isEqualTo(Response.Status.OK.getStatusCode()); + } + // Original table assertThat( createTable( - config.extension, - config.catalog(), - String.format("%s/%s/%s/table_1", baseLocation, config.catalog(), namespace))) - .returns(Response.Status.OK.getStatusCode(), Response::getStatus); + services, String.format("%s/%s/%s/table_1", baseLocation, catalog, namespace))) + .isEqualTo(Response.Status.OK.getStatusCode()); // Unrelated path assertThat( createTable( - config.extension, - config.catalog(), - String.format("%s/%s/%s/table_2", baseLocation, config.catalog(), namespace))) - .returns(Response.Status.OK.getStatusCode(), Response::getStatus); + services, String.format("%s/%s/%s/table_2", baseLocation, catalog, namespace))) + .isEqualTo(Response.Status.OK.getStatusCode()); // Trailing slash makes this not overlap with table_1 assertThat( createTable( - config.extension, - config.catalog(), - String.format("%s/%s/%s/table_100", baseLocation, config.catalog(), namespace))) - .returns(Response.Status.OK.getStatusCode(), Response::getStatus); + services, String.format("%s/%s/%s/table_100", baseLocation, catalog, namespace))) + .isEqualTo(Response.Status.OK.getStatusCode()); // Repeat location assertThat( createTable( - config.extension, - config.catalog(), - String.format("%s/%s/%s/table_100", baseLocation, config.catalog(), namespace))) - .returns(config.response.getStatusCode(), Response::getStatus); + services, String.format("%s/%s/%s/table_100", baseLocation, catalog, namespace))) + .isEqualTo(expectedStatusForOverlaps); // Parent of existing location - assertThat( - createTable( - config.extension, - config.catalog(), - String.format("%s/%s/%s", baseLocation, config.catalog(), namespace))) - .returns(config.response.getStatusCode(), Response::getStatus); + assertThat(createTable(services, String.format("%s/%s/%s", baseLocation, catalog, namespace))) + .isEqualTo(expectedStatusForOverlaps); // Child of existing location assertThat( createTable( - config.extension, - config.catalog(), - String.format( - "%s/%s/%s/table_100/child", baseLocation, config.catalog(), namespace))) - .returns(config.response.getStatusCode(), Response::getStatus); + services, + String.format("%s/%s/%s/table_100/child", baseLocation, catalog, namespace))) + .isEqualTo(expectedStatusForOverlaps); // Outside the namespace - assertThat( - createTable( - config.extension, - config.catalog(), - String.format("%s/%s", baseLocation, config.catalog()))) - .returns(config.response.getStatusCode(), Response::getStatus); + assertThat(createTable(services, String.format("%s/%s", baseLocation, catalog))) + .isEqualTo(expectedStatusForOverlaps); // Outside the catalog - assertThat(createTable(config.extension, config.catalog(), String.format("%s", baseLocation))) - .returns(Response.Status.FORBIDDEN.getStatusCode(), Response::getStatus); + assertThat(createTable(services, String.format("%s", baseLocation))) + .isEqualTo(Response.Status.FORBIDDEN.getStatusCode()); } } diff --git a/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/catalog/io/FileIOExceptionsTest.java b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/catalog/io/FileIOExceptionsTest.java index e92459be7d..59918500f1 100644 --- a/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/catalog/io/FileIOExceptionsTest.java +++ b/dropwizard/service/src/test/java/org/apache/polaris/service/dropwizard/catalog/io/FileIOExceptionsTest.java @@ -23,54 +23,26 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import com.azure.core.exception.AzureException; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.storage.StorageException; import jakarta.ws.rs.core.Response; -import jakarta.ws.rs.core.SecurityContext; -import java.security.Principal; -import java.time.Instant; -import java.util.Date; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.stream.Stream; import org.apache.iceberg.Schema; import org.apache.iceberg.catalog.Namespace; import org.apache.iceberg.rest.requests.CreateNamespaceRequest; import org.apache.iceberg.rest.requests.CreateTableRequest; import org.apache.iceberg.types.Types; -import org.apache.polaris.core.PolarisCallContext; -import org.apache.polaris.core.PolarisDiagnostics; import org.apache.polaris.core.admin.model.Catalog; import org.apache.polaris.core.admin.model.CreateCatalogRequest; import org.apache.polaris.core.admin.model.FileStorageConfigInfo; import org.apache.polaris.core.admin.model.PolarisCatalog; import org.apache.polaris.core.admin.model.StorageConfigInfo; -import org.apache.polaris.core.auth.AuthenticatedPolarisPrincipal; -import org.apache.polaris.core.auth.PolarisAuthorizer; -import org.apache.polaris.core.context.RealmContext; -import org.apache.polaris.core.entity.PolarisEntity; -import org.apache.polaris.core.entity.PrincipalEntity; -import org.apache.polaris.core.persistence.PolarisMetaStoreManager; -import org.apache.polaris.core.persistence.PolarisMetaStoreSession; -import org.apache.polaris.core.persistence.cache.EntityCache; -import org.apache.polaris.service.admin.PolarisServiceImpl; -import org.apache.polaris.service.admin.api.PolarisCatalogsApi; -import org.apache.polaris.service.catalog.IcebergCatalogAdapter; -import org.apache.polaris.service.catalog.api.IcebergRestCatalogApi; -import org.apache.polaris.service.catalog.api.IcebergRestCatalogApiService; -import org.apache.polaris.service.config.RealmEntityManagerFactory; -import org.apache.polaris.service.context.CallContextCatalogFactory; -import org.apache.polaris.service.context.PolarisCallContextCatalogFactory; -import org.apache.polaris.service.persistence.InMemoryPolarisMetaStoreManagerFactory; -import org.apache.polaris.service.storage.PolarisStorageIntegrationProviderImpl; -import org.apache.polaris.service.task.TaskExecutor; +import org.apache.polaris.service.dropwizard.TestServices; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; -import org.mockito.Mockito; import software.amazon.awssdk.services.s3.model.S3Exception; /** Validates the propagation of FileIO-level exceptions to the REST API layer. */ @@ -80,83 +52,14 @@ public class FileIOExceptionsTest { private static final String catalog = "test-catalog"; private static final String catalogBaseLocation = "file:/tmp/buckets/my-bucket/path/to/data"; - private static final RealmContext realmContext = () -> "test-realm"; - private static SecurityContext securityContext; private static TestFileIOFactory ioFactory; - private static IcebergRestCatalogApi api; + private static TestServices services; @BeforeAll public static void beforeAll() { ioFactory = new TestFileIOFactory(); - - InMemoryPolarisMetaStoreManagerFactory metaStoreManagerFactory = - new InMemoryPolarisMetaStoreManagerFactory(); - metaStoreManagerFactory.setStorageIntegrationProvider( - new PolarisStorageIntegrationProviderImpl( - Mockito::mock, () -> GoogleCredentials.create(new AccessToken("abc", new Date())))); - - PolarisMetaStoreManager metaStoreManager = - metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext); - - EntityCache cache = new EntityCache(metaStoreManager); - RealmEntityManagerFactory realmEntityManagerFactory = - new RealmEntityManagerFactory(metaStoreManagerFactory, () -> cache) {}; - CallContextCatalogFactory callContextFactory = - new PolarisCallContextCatalogFactory( - realmEntityManagerFactory, - metaStoreManagerFactory, - Mockito.mock(TaskExecutor.class), - ioFactory); - PolarisAuthorizer authorizer = Mockito.mock(PolarisAuthorizer.class); - IcebergRestCatalogApiService service = - new IcebergCatalogAdapter( - callContextFactory, realmEntityManagerFactory, metaStoreManagerFactory, authorizer); - api = new IcebergRestCatalogApi(service); - - PolarisMetaStoreSession session = - metaStoreManagerFactory.getOrCreateSessionSupplier(realmContext).get(); - PolarisCallContext context = - new PolarisCallContext(session, Mockito.mock(PolarisDiagnostics.class)); - PolarisMetaStoreManager.CreatePrincipalResult createdPrincipal = - metaStoreManager.createPrincipal( - context, - new PrincipalEntity.Builder() - .setName("test-principal") - .setCreateTimestamp(Instant.now().toEpochMilli()) - .setCredentialRotationRequiredState() - .build()); - - AuthenticatedPolarisPrincipal principal = - new AuthenticatedPolarisPrincipal( - PolarisEntity.of(createdPrincipal.getPrincipal()), Set.of()); - - securityContext = - new SecurityContext() { - @Override - public Principal getUserPrincipal() { - return principal; - } - - @Override - public boolean isUserInRole(String s) { - return false; - } - - @Override - public boolean isSecure() { - return true; - } - - @Override - public String getAuthenticationScheme() { - return ""; - } - }; - - PolarisCatalogsApi catalogsApi = - new PolarisCatalogsApi( - new PolarisServiceImpl(realmEntityManagerFactory, metaStoreManagerFactory, authorizer)); + services = TestServices.inMemory(ioFactory); FileStorageConfigInfo storageConfigInfo = FileStorageConfigInfo.builder() @@ -174,17 +77,23 @@ public String getAuthenticationScheme() { .build(); try (Response res = - catalogsApi.createCatalog( - new CreateCatalogRequest(catalog), realmContext, securityContext)) { + services + .catalogsApi() + .createCatalog( + new CreateCatalogRequest(catalog), + services.realmContext(), + services.securityContext())) { assertThat(res.getStatus()).isEqualTo(201); } try (Response res = - api.createNamespace( - FileIOExceptionsTest.catalog, - CreateNamespaceRequest.builder().withNamespace(Namespace.of("ns1")).build(), - realmContext, - securityContext)) { + services + .restApi() + .createNamespace( + FileIOExceptionsTest.catalog, + CreateNamespaceRequest.builder().withNamespace(Namespace.of("ns1")).build(), + services.realmContext(), + services.securityContext())) { assertThat(res.getStatus()).isEqualTo(200); } } @@ -199,7 +108,11 @@ void reset() { private static void requestCreateTable() { CreateTableRequest request = CreateTableRequest.builder().withName("t1").withSchema(SCHEMA).build(); - Response res = api.createTable(catalog, "ns1", request, null, realmContext, securityContext); + Response res = + services + .restApi() + .createTable( + catalog, "ns1", request, null, services.realmContext(), services.securityContext()); res.close(); }